AWS Lambdaの関数URLを使って、Prototype Pollution(プロトタイプ汚染)のサンプルを作ってみた
はじめに
kurenaifさんのプロトタイプ汚染解説動画の分かりやすさに感動して、復習がてら自分でも作ってみようと思い立ちました。
ついでに、AWS Lambdaが自前でURLを持てるようになったと聞いたので、それも試してみることにしました。
リクエストに依存しない例
最初に想定していたのは以下のようなパターンだったのですが、うまくいきませんでした。
- POSTリクエストのボディに入っているJSONを受け取る
- そのJSONを既存のobjectにマージする(このタイミングでObjectが汚染される)
- objectを新しく作り、汚染されたかどうか確認する
試しに以下のような、リクエスト抜きで完結するパターンを試したら、ちゃんと汚染されていました(これを汚染と呼ぶのかは微妙なところですが)。
exports.handler = async (event) => { const obj1 = {}; obj1.__proto__.polluted = 1; console.log("obj1.polluted: " + obj1.polluted); const obj2 = {}; console.log("obj2.polluted: " + obj2.polluted); const response = { statusCode: 200, body: JSON.stringify(obj2.polluted), }; return response; };
リクエストによって汚染される例
どうしてもリクエストによって汚染されてほしいです。もろもろ頑張った結果、以下のコードで汚染されることを確認しました。
exports.handler = async (event) => { const obj1 = clone(JSON.parse(event.body)); console.log("obj1.status: " + obj1.status); const obj2 = {}; console.log("obj2.status: " + obj2.status); const response = { statusCode: 200, body: JSON.stringify(obj2.status), }; return response; }; function isObject(obj) { return obj !== null && typeof obj === 'object'; } function merge(a, b) { for (let key in b) { if (isObject(a[key]) && isObject(b[key])) { merge(a[key], b[key]); } else { a[key] = b[key]; } } return a; } function clone(obj) { return merge({}, obj); }
こちらの記事のコードを引用しました。おそらく merge()
の merge(a[key], b[key])
のところを、自前で実装できていなかったようです。{"__proto__":{"status":"polluted!"}}
のようなJSONを読み込む時、このように再帰しないとプロトタイプのプロパティを適切に設定できないのだと考えています。
調査
merge()
のところで何が起きているか知りたかったので、ログを出すようにしてみました。
function merge(a, b) { for (let key in b) { console.log(isObject(a[key]) + ", " + isObject(b[key])) if (isObject(a[key]) && isObject(b[key])) { console.log("o: " + key + ", " + a[key] + ", " + b[key]) merge(a[key], b[key]); } else { console.log("x: " + key + ", " + a[key] + ", " + b[key]) a[key] = b[key]; } } return a; } function clone(obj) { return merge({}, obj); }
結果、このようなログが出ました。
つまり、以下のようなことが起きている……?
merge({}, event.body)
- 引数は
{}
とevent.body
- forループに入る
{}.__proto__
とevent.body.__proto__
いずれもobjectなので、merge()
が実行されるmerge({}.__proto__, event.body.__proto__)
- forループに入る
{}.__proto__.status
とevent.body.__proto__.status
はいずれもobjectではないので、elseの処理が実行される- 前者はundefined、後者はstring("polluted!")
{}.__proto__.status
にevent.body.__proto__.status
が代入される{}.__proto__
はこの時点で汚染されている?
- forループが終了し、
{}.__proto__
({status:"polluted"}
)が元のプロセスへ返される
- forループに入る
- forループ終了が終了し、(汚染された?)
{}
が返される
- 引数は