pyload-ng: getUserData/get_userdata が Perms.ANY で公開されており、認証されたすべてのユーザーが管理者パスワードをブルートフォース攻撃できる状態になっている。
## 概要
`Api.getUserData` (旧API) と `Api.get_userdata` は、`@permission(Perms.ANY)` で宣言されており、`/api/getUserData` および `/api/get_userdata` でアクセス可能である。`Perms.ANY` が 0 であり、pyLoad の権限チェックがビットマスク AND であるため、このチェックは無効であり、**権限ビットが 0 のアカウントを含む、すべての認証済みアカウントが通過する**。
これらのメソッドは、本来は管理者に限定されるように `check_auth()` をラップしているが、`@permission` が省略されている(`perm_map` にエントリがないため、`is_authorized()` は非管理者に対して `False` を返す)。しかし、これらのラップによって、その保護が解除されている。
したがって、システム上で最も低い権限を持つ攻撃者は、**管理者パスワード**に対する完全な情報を得ることができ、コードベース全体でアカウントロックアウトの仕組みが存在しないため、`X-Forwarded-For` を変更することで、1 分あたり 100 リクエストのレート制限を回避することも可能である。
## 影響を受けるコード
`src/pyload/core/api/__init__.py:1446` および `:1464`
```python
#: Old API
@permission(Perms.ANY)
@get
def getUserData(self, username: str, password: str) -> OldUserData:
"""
`check_auth` と似ているが、UserData 型を返す。
"""
user = self.check_auth(username, password)
...
@permission(Perms.ANY)
@get
def get_userdata(self, username: str, password: str) -> UserData:
user = self.check_auth(username, password)
...
```
## 根本原因
`src/pyload/core/api/__init__.py:57`
```python
class Perms(IntFlag):
ANY = 0 #: 権限を必要としないが、ログインは必要
```
`src/pyload/core/api/__init__.py:108`
```python
def has_permission(user_perms: Perms, required_perms: Perms):
return required_perms == (user_perms & required_perms)
```
`required_perms == 0` の場合、これは `0 == (user_perms & 0)` → `0 == 0` → **常に `True`** と評価されます。したがって、`@permission(Perms.ANY)` デコレータは、どの権限ビットを持っているかにかかわらず、すべての認証済みユーザーを許可します。
以下は、意図された管理者専用の機能である `src/pyload/core/api/__init__.py:1396` との比較です。
```python
@legacy("checkAuth")
@get
def check_auth(self, username: str, password: str) -> dict[str, Any]:
```
`check_auth` は **`@permission` デコレータを持っていません**。`api/__init__.py:1430` の `is_authorized()` は、管理者以外に対して `False` を返します。一方、2つのラッパーは `Perms.ANY` を持ち、すべてのユーザーにアクセスを回復させます。
また、これらのラッパーは `@get` も持っているため、`method_map` に登録され、`/api/<func>` を通じて直接アクセス可能です。
## 脆弱性
**1. ロックアウト機能がない。** コード全体に、ログイン失敗回数のカウント、遅延、またはアカウントの停止機能はありません。ログイン試行の失敗は、攻撃者にとって PBKDF2 の計算コストのみです。
**2. レート制限を回避可能。** `/api/*` には `rate_limit(count=100, period=60)` が適用されています (`src/pyload/webui/app/blueprints/api_blueprint.py:25`)。これは、クライアントが完全に制御できるヘッダー (`src/pyload/webui/app/helpers.py:446`) に基づいて制限を行います。
```python
client_ip = flask.request.headers.get("X-Forwarded-For", "").split(",")[0].strip() \
or flask.request.remote_addr
```
リクエストごとに `X-Forwarded-For` を変更することで、毎回新しい制限バケットが利用可能になります。注目すべきは、**同じファイル** (`helpers.py:288-294`) の `is_loopback_request()` が、`X-Forwarded-For` / `X-Real-IP` / `Forwarded` の存在を明示的に信頼できないものとして扱っている点です。しかし、この保護は `rate_limit()` の内部では適用されていませんでした。
## 影響
管理者のアカウントに対するオンラインブルートフォース攻撃により、完全な管理者権限の奪取につながる可能性があります。また、成功した場合、対象アカウントの `id`、`name`、`email`、`role`、および `permission` ビットが公開される可能性があります。
## 概念実証
0.5.0b3、コミット `a5b008958` で検証済み。
セットアップ:管理ユーザー `pyload` が存在する標準的なインスタンス。さらに、`role=USER`、`permission=0` で作成された、管理者権限を持たないユーザー `bob` が存在します。
**ステップ1:`bob` が本当に権限を持っていないことを確認する。**
```
GET /api/checkAuth?username=pyload&password=pyload -> 401 {"error": "Access denied"}
GET /api/getAllUserData -> 401 {"error": "Access denied"}
```
**ステップ2:脆弱性。同じユーザー、同じセッション、`Perms.ANY` ゲート。**
```
GET /api/getUserData?username=pyload&password=WRONG
-> 200 {"name": null, "email": null, "role": null, "permission": null, "template_name": null}
GET /api/getUserData?username=pyload&password=pyload
-> 200 {"name": "pyload", "email": "", "role": 0, "permission": 0, "template_name": "default"}
```
`role: 0` は `Role.ADMIN` を意味します。`get_userdata` の動作は同じです。これは完璧な合否判定の基準となります。
**ステップ3:ブルートフォース攻撃と復旧。** `bob` (権限 = 0) として実行。管理者パスワードを2文字の値に設定。各リクエストで `X-Forwarded-For` を変更。
```
試行回数 : 667
経過時間 : 39.7秒 (16.8推測/秒)
429 エラー : 0
アカウントロックアウト : なし - セッション全体を通して認証済み
復元されたパスワード : 'zq' (正しい値 'zq') match=True
```
**ステップ4:復元されたパスワードを使用した完全な乗っ取り。**
```
POST /login as pyload/<復元されたパスワード> -> HTTP 302 (管理者として認証)
GET /api/getAllUserData (管理者のみ) -> HTTP 200 (すべてのユーザー情報のダンプ)
```
測定されたスループットは、1スレッドあたり17〜47推測/秒です。制限は、`src/pyload/core/database/user_database.py:14` の PBKDF2-HMAC-SHA256 で、100,000回の反復処理が行われる点であり、レート制限ではありません。
## 推奨される対策
- `getUserData` および `get_userdata` から `@permission(Perms.ANY)` を削除するか、完全に削除してください。これらはレガシー互換のためのものであり、最新の呼び出し元は管理者のみが利用できる `check_auth` を使用しています。
- 構造に関して: `Perms.ANY = 0` とすると、`has_permission()` が意味をなさなくなるため、`@permission(Perms.ANY)` で装飾されたすべてのメソッドが、事実上、ログインしているすべてのユーザーに対して公開されてしまいます。 `ANY` に実際のビット値を割り当てるか、`has_permission()` で明示的に「認証されたセッションが必要」として扱うようにしてください。
- `X-Forwarded-For` ヘッダーは、信頼できるプロキシが明示的に設定されている場合にのみ信頼してください。それ以外の場合は、`rate_limit()` を `request.remote_addr` に対して適用するか、`is_loopback_request()` で使用されているのと同じガードを追加してください。
- アカウントごとの認証失敗時のスロットリングまたはロックアウト機能を追加してください。
- `_check_password()` 関数(`src/pyload/core/database/user_database.py:61` でハッシュ値を比較する際に単純な `==` を使用しており、2つ前のファイルにある「常に `compare_digest` を使用する」という推奨事項と矛盾しています)で、`hmac.compare_digest()` の使用を検討してください。
## トリアージに関する注意点
PyPI には `0.5.0b3` のリリースはありません。 `develop` ブランチは、開発ビルドを自動的に公開します(`setup.py:86` で `VERSION` ファイルに `.dev<build>` を追加します)。テスト時点での最新バージョンは `0.5.0b3.dev101` でした。 `Perms.ANY = 0` という設計は以前から存在しますが、ここでは `0.5.0b3.dev*` のバージョンのみが検証されています。最終的なバージョン範囲を決定する前に、`0.5.0b2.*` および `0.4.x` のバージョンも影響を受けるかどうかを確認してください。
影響を受けるパッケージ:
- pip (Python) pyload-ng: 影響を受けるバージョンは >= 0.5.0b3.dev1、<= 0.5.0b3.dev101、修正は未実施。