JavaScriptの型混同:入力検証の回避とその修正方法
Alessio Della Libera
2021年11月3日
0 分で読めます以前のブログ記事では、型の操作(型混同)によってテンプレートサンドボックスを脱出し、クロスサイトスクリプティング(XSS)やコードインジェクションの脆弱性につながる可能性があることを紹介しました。
今回の調査の主な目的の1つは、JavaScriptエコシステムにおいて、型混同攻撃(予期しない型の入力を与える攻撃)でセキュリティ修正や入力検証を回避できるかどうか、またその方法を探ることでした。
まず、プロトタイプ汚染やXSSなどの一般的なセキュリティ脆弱性がどのように修正されているかを調査しました。この調査には、業界最大のオープンソース脆弱性データベースであるSnyk Intel Vulnerability Databaseのデータを活用しました。この情報から、さまざまなメンテナーが特定の種類の脆弱性を防ぐために用いるパターンをいくつか特定できました。
次に、型混同の脆弱性を見ていきましょう。この記事では、予期しない型の入力を与えることで、入力のサニタイズや検証を回避できる一般的なシナリオを紹介します。また、この種の入力検証の回避を修正したいオープンソースメンテナー向けに、修正例や提案も紹介します。
JavaScriptの基礎
配列値を使って入力検証を回避し、潜在的なセキュリティ脆弱性につなげる方法を説明する前に、まずJavaScriptの基本的な仕組みを確認しましょう。特に、後ほどプロトタイプ汚染の事例を取り上げる際に役立ちます。
値の比較:===と==
JavaScriptでは、値の比較に使える演算子が2つあります。
厳密等価演算子
===等価演算子
==
主な違いの1つは、===演算子は両方のオペランドの型が異なる場合、常にfalseを返すことです(型変換は行いません)。一方、==演算子では、オペランドの型が異なる場合、まず共通の型に変換してから比較します。これらの演算子の詳しい動作については、公式ドキュメントの厳密等価演算子と等価演算子をご覧ください。
次の例で、この動作を確認できます。
同じ値と型を持つオペランドを比較する場合、==演算子と===演算子はどちらもtrueを返します(2行目と3行目)。一方、もう一方のオペランドと同じ文字列表現を持つ配列を比較する場合、==演算子を使ったときだけtrueを返します。6行目では、値["test"]がまず文字列表現に変換され、もう一方のオペランド"test"と比較されます。オペランドの型が異なる場合、===演算子は常にfalseを返します(7行目)。
注:上記の考え方は、不等価演算子!==と!=にも当てはまります。
値の文字列表現を取得する方法には、次のようなものがあります。
toString()メソッドを呼び出す(2行目)String関数を使って文字列に変換する(2行目)+演算子を使って値と空文字列を連結する(3行目)
配列値の文字列表現は、各要素をカンマで連結したものです。
プロパティアクセサー
JavaScriptのもう1つの重要な機能として、ブラケット記法でオブジェクトのプロパティにアクセスする際、文字列以外の型の値も指定できます。値が文字列(またはSymbol)でない場合は、まず文字列に変換してからオブジェクトのプロパティへのアクセスに使用されます。
次の例を見てみましょう。
このように、プロパティへのアクセスに使うキーは、さまざまな型にできます。4行目では、プロパティ["test"]がまず文字列(つまり"test"という値)に変換され、その結果がオブジェクトのプロパティにアクセスするキーとして使われます。実際、6行目で文字列"test"を使うと、4行目で設定した値を取得できます。
同様に、文字列表現が[object Object]となる空のオブジェクト{}(9行目)を使ってオブジェクトの値を書き込んだ場合、文字列[object Object]を直接指定することで同じ値にアクセスできます(11行目)。
StringとArrayのメソッド
StringとArrayの両方に定義され、同じ名前を持つ組み込みメソッドがあります。たとえばincludes、indexOf、lastIndexOfなどです。
この記事でこれが重要なのはなぜでしょうか。これらのメソッドは、呼び出し対象の入力型によって動作が異なります。いずれかのメソッドをユーザー入力の検証に使っている場合、型の異なる入力(たとえば配列)が与えられ、型のチェックが行われなければ、検証を回避されるおそれがあります。
次の例では、StringとArrayの両方に定義された組み込みメソッドの動作の違いを示します。
注:この例の値("<script>"、["<script>"]、[["<script>"]])はすべて同じ文字列表現になります。
たとえば3行目では、String.prototype.indexOf()メソッドが呼び出されます。このメソッドは文字列に文字<が含まれているか確認し、最初に見つかった位置を返します。含まれていない場合は-1を返します。
しかし、入力が配列の場合はArray.prototype.indexOf()メソッドが呼び出され、配列に要素<が含まれているか確認します。配列には文字列"<script>"という要素が1つしかないため、この関数は-1を返し、その文字に対するサニタイズが回避される可能性があります。
JavaScriptの基本概念のまとめ
ここまでの内容をまとめると、次のとおりです。
オペランドの型が異なる場合、
===演算子は常にfalseを返すブラケット記法を使うと、異なる型のキーでオブジェクトのプロパティにアクセスできる
StringとArrayの両方に定義された一部の組み込みメソッド(indexOfやincludesなど)は、入力の型によって動作が異なる
次に進む前に、ここまでに取り上げたメソッドを使ってユーザー入力を検証する、次のシナリオを見てみましょう。
次のチェックだけでは、オブジェクトobjにキーisAdminが設定されるのを防げない可能性があります(変数propはユーザーが制御できるものとします)。
propの値を"isAdmin"に変更すると、上記の例では例外が発生します。
次は、入力に危険な文字が含まれていないかを検出し、潜在的なXSSを防ぐために使われる、もう1つの「弱い」チェックの例です。
このような回避が可能なのは、特定の条件を満たす場合に限られる点が重要です。たとえば、入力が特定のソースから来ており(次のセクションを参照)、入力に対してStringにのみ定義された組み込みメソッドが呼び出されないことが条件です。そうでなければ、他の型では定義されていないため、アプリケーションはエラーや例外を返します。
次の例では、入力に対してtoLowerCase()メソッドを呼び出しています。配列値によって===の比較を回避できることはすでに見ましたが、配列が渡された場合、toLowerCase()は文字列にしか定義されていないため、アプリケーションは例外をスローします。
配列値を取得する方法
ここで、リモートデータから配列値を取得するにはどうすればよいのか、疑問に思うかもしれません。
配列値を取得する方法はいくつかあります。
サーバーサイド:一般的なExpressフレームワークを使っている場合、
req.queryやreq.body(express.json()ミドルウェアを使用している場合)の値は、文字列だけでなく配列やオブジェクトとしても解析されることがあります。クライアントサイド:
postMessageAPIから渡されるデータ。
サーバーサイド
GETリクエストの場合、一般的なExpressフレームワークを使い、値がreq.queryから渡されるなら、parameter[]=valueという表記で配列値を取得できます。一般に、これは一般的なquerystring解析ライブラリであるqsを使用するあらゆるアプリケーションに当てはまります。
POSTリクエストの場合、express.json()ミドルウェアを使用すると、リクエスト本文はJSONオブジェクトとして解析されます。
次のExpressアプリケーションでは、配列値を取得する方法を示します。
クライアントサイド
postMessage APIから渡されるデータも、文字列以外の型にできます。
上記のコードをテストするには、ブラウザーの開発者コンソールを開き、次を実行します:window.postMessage({name: ["array"]}, "*")。次の出力が表示されます。

調査結果
今回の調査で発見し、報告した問題の一部を以下に示します。
モジュール | 脆弱性 | Snykアドバイザリ | CVE |
|---|---|---|---|
object-path | プロトタイプ汚染 | CVE-2021-23434 | |
immer | プロトタイプ汚染 | CVE-2021-23436 | |
mpath | プロトタイプ汚染 | CVE-2021-23438 | |
set-value | プロトタイプ汚染 | CVE-2021-23440 | |
edge.js | クロスサイトスクリプティング(XSS) | CVE-2021-23443 | |
jointjs | プロトタイプ汚染 | CVE-2021-23444 | |
datatables.net | クロスサイトスクリプティング(XSS) | CVE-2021-23445 | |
teddy | クロスサイトスクリプティング(XSS) | CVE-2021-23447 |
脆弱性の開示に従い、複数のメンテナーに非公開で責任ある形で連絡しました(現在も開示プロセス中の調査結果があります)。
何よりも、連絡を差し上げたすべてのメンテナーの皆さまに、ご対応いただいたことへの感謝を申し上げます。
次の事例では、配列値を渡すことで、プロトタイプ汚染の修正とXSSの入力検証をどのように回避できたのかを説明します。
事例:object-pathのプロトタイプ汚染
object-pathは、パスを使って深い階層にあるプロパティにアクセスするためのライブラリです。このライブラリは配列形式のパスも受け付けるため、['part1', 'part2', etc.]の形式でパスを指定できます。このライブラリにはすでにプロトタイプ汚染の脆弱性(CVE-2020-15256)がありました。導入された修正は次のとおりです。
この修正では、パスに危険な要素が含まれている場合(それらが文字列であれば)、正しく例外をスローします。実際、次のペイロードを使うと、関数はエラーをスローします。
しかし、先ほど見たように、次のことができます。
ブラケット記法でオブジェクトのプロパティにアクセスする場合、任意の型のキーを使用できる
オペランドの型が異なる場合、
===演算子はfalseを返します。条件currentPath === '__proto__'は、currentPathが['__proto__']の場合falseを返します(constructorの場合も同様です)。
つまり、危険なキーをそのキー自身だけを含む配列でラップすれば修正を回避でき、プロトタイプ汚染を引き起こすことができます。
修正方法
Snykは2021年8月25日にメンテナーへ非公開で連絡し、この問題は2021年8月27日にv0.11.6で速やかに修正されました。導入された修正では、チェックの前にパスの各要素を文字列に変換することで、このシナリオを防ぎます(数値型または文字列型でない場合)。
前述のペイロードは、もう機能しません。
ケーススタディ:edge.jsにおけるクロスサイトスクリプティング(XSS)
edge.jsはNode.jsのテンプレートエンジンです。このライブラリには、XSSなどのセキュリティ脆弱性を防ぐために有効化できる機能が組み込まれています。特に、ドキュメントによると、「補間(波括弧内のコード)の出力は、XSS攻撃を防ぐためにHTMLエスケープされます」。XSSを防ぐためにHTMLの危険な文字をエスケープする元の関数は、次のとおりです。
ご覧のとおり、inputがstring型の場合にのみHTMLエスケープされます。つまり、ユーザーが制御できる入力がオブジェクト型(配列など)で、SafeValueではない場合、テンプレートで{{ }}を使用しても、エスケープされずに返されます(エラーや例外も発生しません)。その結果、XSSの脆弱性につながる可能性があります。
これがどのように悪用されるかを示すために、このライブラリをテンプレートエンジンとして使用し、ユーザーが制御する値をテンプレートに描画する次のWebアプリケーションを見てみましょう。
ファイルviews/welcome.edgeの内容は次のとおりです。
前述のとおり、特定のソースから取得したデータは、異なる型になる場合もあります。URLhttp://localhost:3000/test?name=%3Cimg%20src=x%20onerror=%27alert(1)%27%20/%3Eにアクセスすると、関数は入力をエスケープします。しかし、別のURL(括弧[]に注目)http://localhost:3000/test?name[]=%3Cimg%20src=x%20onerror=%27alert(1)%27%20/%3Eにアクセスすると、XSSを引き起こすことができます。req.query.nameパラメーターが配列として解析され、エスケープされないためです。
修正方法
Snykは2021年9月1日にメンテナーへ非公開で連絡し、この問題はv5.3.2で速やかに修正されました。導入された修正では、SafeValue型以外の入力をすべてエスケープすることで、このケースに対処しています。
このような状況を防ぐ別の方法として、エスケープする前に入力を文字列に変換する(上記の例のように入力の型を確認せずに済む)方法や、入力が文字列型でない場合に空文字列(またはエラーメッセージ)を返す方法があります。
対象者別のポイント
開発者
サニタイズに===演算子を使う場合は、両方のオペランドが同じ型であることを確認することが重要です。入力をエスケープする場合は、入力が文字列型でないケースにも対処する必要があります(特に、文字列以外の値に対する関数の挙動が文書化されていない場合)。
メンテナー
関数やAPIが入力のサニタイズを担う場合、入力の型に対応していないのであれば、ユーザーの混乱を避けるため、その挙動を文書化することが重要です。メンテナーは、文字列の場合の入力エスケープなど、一部のサニタイズの問題には対処していても、入力が文字列でない場合への対応は行っていないことがあります。
セキュリティ研究者
今回はプロトタイプ汚染とXSSの脆弱性に注目しましたが、この攻撃手法によって既存のサニタイズを回避し、セキュリティ上の問題につながる別のケースやシナリオも存在すると考えています。弊社プログラムの対象となっているオープンソースプロジェクトで同様の問題(またはその他のセキュリティ脆弱性)を見つけた場合は、Snyk Vulnerability Disclosureフォームからぜひご報告ください。
参考資料
プロトタイプ汚染:Arteau, Oliver. “JavaScript prototype pollution attack in NodeJS application.” GitHub、2018年5月26日
CTFを始めよう
オンデマンドのバーチャル入門ワークショップを視聴して、CTFの課題の解き方を学びましょう。



