pyLoadにおいて、APIキーの検証(`check_apikey`キャッシュ)に認証回避の脆弱性があります。
## 概要
`check_apikey()`関数内のAPIキーキャッシュは、同じ`key_id`に対して有効なキーが最近使用された場合、攻撃者が**偽のAPIキー**を使用して認証を通過できる可能性があります。
キャッシュヒット時には、APIキーの**シークレットが一切検証されず**、キャッシュの有効期間中、認証が完全に回避されます。
---
# 影響を受けるコンポーネント
- `src/pyload/core/api/init.py`
- `Api.check_apikey()`
- `src/pyload/core/database/apikey_database.py`
- `check_apikey()`
- `_check_key()`
通常のAPIリクエストでは、以下のファイルがキャッシュを使用します。
- `src/pyload/webui/app/helpers.py`
これは、以下のコードを呼び出します。
```python
api.check_apikey(api_key)
```
この際、`ttl`(有効期間)が指定されていないため、デフォルトの**300秒のキャッシュ**が常に有効になります。
---
# 根本原因
APIキーの検証が成功した場合、以下の場所にキャッシュされます。
```python
self._apikey_cache
```
キャッシュの構造は以下の通りです。
```python
{
key_id: (timestamp, cached_data)
}
```
キャッシュキーは**`key_id`のみに基づいており**、この値はユーザーが提供したAPIキーから直接取得されます。信頼できる参照元から取得されるものではありません。
キャッシュヒットが発生した場合、`check_apikey()`関数は以下の値を返します。
```python
{
"success": True,
"data": cached_data
}
```
この際、以下の項目のみが確認されます。
- `expires_at`
この処理パスのどの段階でも、提供されたシークレット:
```text
apikey[-43:]
```
は、保存されているキーのハッシュ値と比較されません。
---
実際のシークレットの検証は、以下の関数でのみ行われます。
```python
_check_key()
```
この関数では、正しく以下の関数が使用されています。
```python
hmac.compare_digest()
```
ただし、この関数は、データベース層の`check_apikey()`の実装によってのみ呼び出される**キャッシュミス**の場合に限られます。
さらに、キャッシュされたオブジェクトは、意図的に保存されているキーのハッシュを含んでいないため、検証に使用できません。
キャッシュされたレコードには、以下の情報のみが含まれています。
- `id`
- `user_id`
- `name`
- `created_at`
- `expires_at`
- `last_used`
`key_hash` が存在しないため、キャッシュは提示されたシークレットを検証するために必要な情報を持っていません。
その結果、正当なリクエストがキャッシュに登録されると、その後の同じ `key_id` を使用したすべてのリクエスト(300秒のキャッシュ期間内)は、**シークレットの検証なしに**受け入れられます。
---
# 影響
`key_id` の値は、APIキーが作成される際に割り当てられる小さな連続した整数です(1, 2, 3, ...)。そのため、簡単に列挙することができます。
APIキーの形式は、ソースコードから公開されているため、再構築可能です。
したがって、攻撃者は有効なAPIキーのシークレットを知る必要はありません。
必要なのは、過去5分以内に正当なキーが使用された `key_id` であることです。これは、アクティブに使用されている環境で発生する可能性が高いです。
そのキャッシュ期間中、偽造されたAPIキーは、正当なキーと同じ認証パスをたどり、関連するユーザーの権限を継承します。
これにより、**完全なリモート認証の回避**が発生します。
この問題は、**重大(Critical)**な深刻度であると分類します。
---
# 推定されるCVSS v3.1
```
AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H
```
(`AC:H` は、正常な攻撃にはキャッシュにすでにエントリが存在する必要があるためです。)
---
# 再現手順
### 1. 管理者アカウントを作成します。
---
### 2. APIキーを生成します。
例:
```text
pl_11<43文字のシークレット>
```
ここで:
```
key_id = 1
```
---
### 3. 正当な認証済みリクエストを送信します。
```bash
curl \
-H "X-API-Key: pl_11<実際の43文字のシークレット>" \
http://127.0.0.1:8000/api/status
```
期待される応答:
```text
HTTP/1.1 200 OK
```
---
### 4. 300秒以内に、偽造されたAPIキーを使用して同じリクエストを送信します。
```bash
curl \
-H "X-API-Key: pl_11xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
http://127.0.0.1:8000/api/status
```
観察された応答:
```text
HTTP/1.1 200 OK
```
APIキーのシークレットが完全に間違っているにもかかわらず、リクエストは成功します。
---
# 制御テスト
偽造されたAPIキーが、有効なリクエストによってキャッシュが更新される前に送信された場合、サーバーは正しく以下の応答を返します。
```text
HTTP/1.1 401 Unauthorized
```
正常に動作する偽造リクエストと、失敗する偽造リクエストとの唯一の違いは、キャッシュにその`key_id`に対するエントリが既に存在するかどうかです。
これにより、脆弱性はキャッシュヒットのパスに限定されることがわかります。
---
# 推奨される修正方法
キャッシュヒットは、**絶対にシークレットの検証を置き換えてはなりません。**
考えられるアプローチとしては、以下があります。
- キャッシュに、完全なAPIキー(またはそのハッシュ)から派生した値を保存し、キャッシュヒットのたびに`hmac.compare_digest()`を使用して検証します。
- または、`key_id`だけでなく、**完全なAPIキーのシークレット**から派生した値(例えば、完全なAPIキーの暗号学的ハッシュ)をキーとしてキャッシュを使用します。
実装方法に関わらず、セキュリティの原則は以下のままにする必要があります。
> **APIキーのシークレットは、キャッシュがヒットしたかどうかにかかわらず、すべての認証済みリクエストで検証する必要があります。**
影響を受けるパッケージ:
- pip (Python) pyload-ng: 影響を受けるバージョンは0.5.0b3.dev101より前のバージョン、修正されたバージョンは0.5.0b3.dev101。