ApostropheCMS: `@apostrophecms/db-connect`における認証済みユーザーによるプロジェクション除外機能のプロトタイプメンバー削除が、システム全体でのサービス拒否を引き起こす。
## 概要
`@apostrophecms/db-connect`は、SQLite、PostgreSQL、およびマルチPostgresアダプタで使用される、インメモリのMongoDBエミュレーションを搭載しています。その`unsetNestedField`ヘルパー関数は、攻撃者が制御するドット表記のフィールドパスを検証なしに辿り、解決されたプロパティに対して`delete`関数を呼び出します。権限の低い認証済みユーザー(`view-draft`ロールを持つユーザー、つまりすべての編集者と貢献者、および`guestApiAccess`が有効になっている場合は、ログインしているすべてのユーザー)は、`project`クエリビルダーに`constructor.prototype.hasOwnProperty`のような除外キーを含むREST GETリクエストを送信できます。SQLiteまたはPostgreSQLアダプタの場合、このキーは`unsetNestedField`に到達し、`Object.prototype`を解決して、共有ランタイムメソッドを削除します。このメソッドがNodeプロセスの全体で共有されているため、削除によってすべてのテナントとユーザーに対するリクエスト処理が中断され、プロセスが再起動されるまで、単一のリクエストによって持続的なサービス拒否が発生します。
## 根本原因
投影クエリビルダーは、投影値のみを処理し、投影キーは処理しません。そのため、任意のドット付きキーがデータベース層まで引き継がれます。SQLiteおよびPostgreSQLのアダプターでは、`applyProjection`というJavaScript関数によって除外投影が適用されます。この関数は、除外された各フィールドに対して`unsetNestedField(result, field)`を呼び出します。`unsetNestedField`は、フィールドを`.`で分割し、結果として得られたセグメントを、保護なしで走査します。この関数は、兄弟関数である`setNestedField`とは異なり、リテラルな`__proto__`さえもブロックしません。また、どちらのヘルパー関数も`constructor`や`prototype`をブロックしません。動作対象のオブジェクトは、`JSON.parse(JSON.stringify(doc))`によって生成されたプレーンなオブジェクトであるため、その`constructor`はグローバルな`Object`を指します。したがって、`constructor.prototype.<member>`というパスで共有の`Object.prototype`に到達し、コアとなる設定可能なメソッドが削除されてしまいます。問題となっている点は、信頼できない入力が、共有プロトタイプオブジェクトにアクセスして変更できてはならないということです。
```text
影響を受ける関数(リポジトリ内の相対パス):
- unsetNestedField packages/db-connect/lib/shared.js:119-129 (セグメントの保護なし;解決されたプロパティへの削除)
- applyProjection packages/db-connect/lib/shared.js:164-216 (除外の処理が、除外されたキーごとにunsetNestedFieldを呼び出す)
到達可能性のトリガー(SQLアダプターのコンシューマー):
- project.launder / project.finalize packages/apostrophe/modules/@apostrophecms/doc-type/index.js:2003-2103
- getRestQuery packages/apostrophe/modules/@apostrophecms/piece-type/index.js:1139-1157
- SQLiteアダプター applyProjectionの呼び出し packages/db-connect/adapters/sqlite.js:906,980
```
## 影響
前提条件:`view-draft`権限を持つ認証済みアカウント(すべての編集権限を持つユーザー、または`guestApiAccess: true`の場合、ログインしているすべてのユーザー)、およびSQLite、PostgreSQL、またはmultipostgresアダプタを使用するように設定されたコンシューマー。デフォルトの`mongodb`アダプタは、この処理を実際のMongoDBドライバに引き継ぎ、影響を受けません。これらの条件のもと、巧妙に作成されたGETリクエストによって、攻撃者が選択した共有プロトタイプメソッド(例えば、`Object.prototype.hasOwnProperty`、`toString`、または`Array.prototype`のメンバー)が削除されます。この脆弱性は、プロセス全体にわたる永続的なサービス拒否を引き起こします。削除後、`obj.hasOwnProperty(...)`のような通常の呼び出しがApostrophe、その依存関係、およびデータベースドライバ全体でエラーを引き起こし、影響を受けたNodeプロセスは再起動されるまで、どのユーザーからのリクエストも処理できなくなります。攻撃者はどのメンバーを削除するかを選択し、共有ランタイム状態の破損はプロセスの存続期間中持続します。
## 実証
「この問題は、出荷されたヘルパーを公開されているdb-connectソースコードに対して直接実行することで再現されました。ライブコンシューマーには、SQLiteまたはPostgreSQLアダプタ、認証済みの`view-draft`セッション、および少なくとも1つの対応するドキュメントが必要です。」
1. `sqlite`、`postgres`、または`multipostgres`アダプタを使用しているサイトで、`view-draft`権限を持つ任意のユーザーとして認証します。
2. 共有プロトタイプメソッドをターゲットとする除外プロジェクションを指定した、REST読み取りリクエストを送信します。
3. その後、共有メソッドが削除されたため、プロセス全体でリクエストが失敗することを確認します。
```text
GET /api/v1/@apostrophecms/article?project[constructor.prototype.hasOwnProperty]=0
Cookie: <認証済みのview-draftセッション>
```
```text
safe benign exclusion {title:0} leaves Object.prototype.hasOwnProperty intact
VULNERABLE exclusion projection deleted Object.prototype.hasOwnProperty (callableBefore=true, brokeEveryObject=true)
VULNERABLE exclusion projection deleted an arbitrary Object.prototype method (__pocMarker)
```
## 攻撃の連鎖
1. **脆弱性:** 認証済みの `view-draft` 権限を持つユーザーが、REST APIのGETエンドポイント `GET /api/v1/@apostrophecms/<piece-type>` を呼び出します。このエンドポイントは、どの編集権限を持つユーザーからもアクセス可能です。
2. **制御:** クエリ文字列 `project[constructor.prototype.hasOwnProperty]=0` が、`qs` ( `allowDots` オプションがオフ) によって解析され、リテラルなドットを含む単一のキー `{ 'constructor.prototype.hasOwnProperty': '0' }` となります。
3. **経路:** `getRestQuery` はまずクエリビルダーを適用します。`project.launder` はキーをそのままコピーし、値のみを `false` に変換します。`project.finalize` は、`if (!val) continue` によって `false` の値をスキップし、キーを保持します。カーソルはこのキーをプロジェクションとして保存します。
4. **防御:** 攻撃者のプロジェクションを破棄する `publicApiProjection` のオーバーライドは、`canAccessApi(req)` が `false` の場合にのみ実行されます。したがって、認証済みの呼び出し元に対してはスキップされます。この経路には、キーの許可リスト、セグメントチェック、またはプロトタイプガードは存在しません。
5. **プリミティブ:** SQLiteまたはPostgreSQLのアダプターでは、`applyProjection` が除外の処理を行い、`unsetNestedField(result, 'constructor.prototype.hasOwnProperty')` を呼び出します。これにより、`Object.prototype` が解決され、`delete Object.prototype.hasOwnProperty` が実行されます。
6. **結果:** Nodeプロセスの全体で共有プロトタイプメソッドが削除されるため、すべてのユーザーとテナントに対してリクエスト処理がエラーとなり、再起動されるまで継続します。
## 突破の証拠
関連する保護機能は個別に検証され、いずれもこの経路を遮断していません。`launder`という処理(packages/apostrophe/modules/@apostrophecms/doc-type/index.js:2003-2011)は、値のみを処理し、任意のキーを保持します。`publicApiProjection`の権限オーバーライド(packages/apostrophe/modules/@apostrophecms/piece-type/index.js:1142-1156)は、`!canAccessApi(req)`によって制御されており、認証された`view-draft`の呼び出しでは適用されません。`finalize`のキーごとのループ(doc-type/index.js:2062-2069)は、キーを検証する前に、偽の値が設定されている場合に処理を中断します。最も重要な点として、`unsetNestedField`(packages/db-connect/lib/shared.js:119-129)には、セグメントガードが全くありません。この関数の兄弟関数である`setNestedField`は、リテラルな`__proto__`をブロックしますが、どちらのヘルパー関数も`constructor`や`prototype`をブロックしません。また、`constructor.prototype.<member>`の経路は、たとえ部分的な`__proto__`チェックが行われていても、それを回避します。この発見を否定しようとする試みはすべて失敗しました。値を包含ブランチにルーティングしようとしても(これは`setNestedField`を使用します)、値が偽であるため、その処理は行われません。処理対象のオブジェクトは、`JSON.parse(JSON.stringify(doc))`によって生成された単純なオブジェクトであり、その`constructor`はグローバルな`Object`です。また、`Object.prototype.hasOwnProperty`は設定可能であるため、削除は成功します。プロトタイプ汚染の修正として追加されたセグメントガードは、`{__proto__, constructor, prototype}`を含んでいますが、これは`apostrophe`という別のパッケージ内の`apos.util.set`/`get`でのみ適用されており、`db-connect`のヘルパー関数には適用されていません。
## 影響を受けるバージョン
- `Ecosystem: npm`
- `Package: @apostrophecms/db-connect`
- `確認された影響範囲: >= 1.0.0, <= 1.0.2`
- `確認された最新リリース: 1.0.2 (npm registry)`
- `修正状況: 未修正`
公開されている安定版1.0.0、1.0.1、および1.0.2のリリースにおいて、`unsetNestedField`と`applyProjection`の呼び出しは、バイト単位で完全に同一です。また、SQLiteとPostgreSQLのアダプタは、いずれも返されたドキュメントごとに`applyProjection`を呼び出します。どの公開版リリースにも、この問題に対する修正は含まれていません。プレリリース版(1.0.0-alpha.1、1.0.0-alpha.2、1.0.2-alpha.0)も同じコードを含んでいますが、サポート対象の安定版チャンネルの範囲外です。この問題が発生するには、SQLite、PostgreSQL、またはマルチPostgreSQLアダプタを使用している必要があります。デフォルトの`mongodb`アダプタは、プロジェクションを実際のMongoDBドライバに委譲するため、影響を受けません。この問題を引き起こす`apostrophe`パッケージは、`@apostrophecms/db-connect@^1.0.1`を依存関係として宣言しており、これが影響を受ける1.0.2に解決されます。
## 推奨される修正
信頼できないフィールドパスが、共有プロトタイプオブジェクトにアクセスできないようにする必要があります。`packages/db-connect/lib/shared.js`にある`unsetNestedField`と`setNestedField`の両方において、パスのセグメントに`__proto__`、`constructor`、または`prototype`が含まれる場合は、エラーとするようにする必要があります。これは、`apostrophe`パッケージの`apos.util.set`/`get`で使用されている`unsafePathSegments`ガードと同様のものです。あるいは、ヘルパー関数が自身のプロパティのみを操作するように変更することもできます(例えば、`Object.prototype.hasOwnProperty`チェックを使用するか、`Map`ベースのアクセスを使用するなど)。これにより、プロトタイプオブジェクトへのアクセスを防止できます。`applyProjection`が問題の発生箇所であるため、ヘルパーレベルでガードを追加することで、すべてのアダプタパスを一度に保護できます。コードの変更なしで一時的な対策として、デフォルトの`mongodb`アダプタを使用するか(これは影響を受けません)、認証されたAPIへのアクセスを制限して、完全に信頼されたロールのみが、ピースタイプのRESTエンドポイントにアクセスできるようにすることができます。
報告者:zx (GitHub: @manus-pi)。
影響を受けるパッケージ:
- npm (JavaScript) @apostrophecms/db-connect: 影響を受けるバージョンは1.1.0未満、1.1.0で修正済み
- npm (JavaScript) apostrophe: 影響を受けるバージョンは4.33.0未満、4.33.0で修正済み