Skip to main content

`marked`のXSS脆弱性を修正する

2016年5月15日

0 分で読めます

数週間前、人気のクロスサイトスクリプティング(XSS)脆弱性を、markedパッケージの脆弱性データベースに追加しました。この記事では、脆弱性の内容とサンプルアプリでの悪用方法を説明し、アプリケーションで問題を修正する方法を紹介します。

markedはMarkdownを解析してHTMLに変換するため、ユーザーコメント、商品レビュー、サポートへの問い合わせなどのユーザー入力を、リンク、太字、斜体などに対応したリッチなテキストに簡単に変換できます。MarkdownはJavaScriptをサポートしていないため、クロスサイトスクリプティングの影響を受けず、ユーザー入力の表示に安全だと考えられることがよくあります。

しかし実際には、MarkdownによってXSSのリスクは低減されても、完全になくなるわけではありません。markedにあったこの簡単に悪用できるXSS脆弱性は、その違いを示す厳しい教訓です。

「オープンソースセキュリティの現状 2019」で示したように、XSS脆弱性の報告は増え続けています。報告の大半はPHPのPackagistエコシステムに寄せられ、次いでnpm、Maven Centralと続きます。

脆弱性の内容

Markdownはスクリプトをサポートしていませんが、marked(ほかのMarkdownクライアントと同様)はインラインHTMLをサポートしています。インラインHTMLには、攻撃者が悪意のあるスクリプトを挿入するために利用できるタグが含まれる場合があります。markedはユーザー入力をページに表示するためによく使われることから、開発者はこの問題に対処するためのセキュリティオプションを追加しました。このパッケージには、HTMLや危険な入力を検出し、エンコードまたは削除するsanitizeオプションがあります。

残念ながらsanitizeはデフォルトで無効ですが、アプリで有効にできます。次の例は、sanitizeオプションの動作を示しています。

var marked = require('marked');
console.log(marked('<script>alert(1)</script>'));
// Outputs: <script>alert(1)</script>

marked.setOptions({sanitize: true});
console.log(marked('<script>alert(1)</script>'));
// Outputs: <p><script>alert(1)</script></p>

HTMLを検出することは重要ですが、サニタイズはそれだけでは終わりません。Markdownはスクリプトをサポートしていないものの、リンクには対応しているため、javascriptリンク(例:javascript:alert(1))が作られる可能性があります。ユーザーがクリックすると被害が生じるおそれがあります。sanitize機能はこの点を考慮し、javascript:で始まるように見えるリンクを削除します。コロンのHTMLエンティティ:を使ったリンク(例:javascript&58;alert(1))も削除します。しかし、この対策をもってしても、見逃されるケースが1つあります……

HTMLの形式には厳密な制約がなく、ブラウザーはHTMLを非常に寛容に処理します。その一例として、HTMLエンティティの処理では末尾のセミコロンを必須とせず、:と:のどちらも受け入れます。一方、markedのサニタイズではセミコロンが必須で、見つからない場合、その文字列は単なるテキストとして扱われます。つまり、:は削除されますが、&58this;はそのまま出力されます。攻撃者はこの手法でmarkedのチェックを回避しながら、ブラウザーにスクリプトを実行させることができます。

sanitizeが機能する場合としない場合を、コードで示します。

var marked = require('marked');
marked.setOptions({sanitize: true});

// Naive attempt - fails.
console.log(marked('[Gotcha](javascript:alert(1))'));
// Outputs: <p>)</p>

// Evasion attempt using '&#58;' instead of ':' - fails.
console.log(marked('[Gotcha](javascript:alert(1))'));
// Outputs: <p></p>

// Evasion attempt using '&#58;' (note the 'this') instead of ':' - SUCCEEDS
console.log(marked('[Gotcha](javascript:alert(1))'));
// Outputs: <p><a href="javascript&#58this;alert(1&#41;)">Gotcha</a></p>
// Same as: <p><a href="javascript:this;alert(1);">Gotcha</a></p>

ブラウザーは:を:と同じように解釈するため、クリックするとスクリプトが実行されます。もちろん、ここで使ったスクリプト自体には大した意味はありません。しかし攻撃者は、より高度なペイロードを挿入し、ブラウザーの同一オリジンポリシーを破って、XSSによる被害を引き起こす可能性があります。

Goofでのライブエクスプロイト

mongooseのBufferに関する脆弱性を取り上げたときと同様に、この脆弱性を脆弱なアプリケーションGoofに追加しました。脆弱性を実際に悪用して、脆弱なコードを確認することで、問題への理解が深まります。Goofはクローンし、GitHubの手順に従って実行できます。

GoofはTODOアプリケーションで、メモ内のMarkdownに対応するためにmarkedを使っています。Goofは最高クラスのTODOアプリです。リンク、太字、斜体に対応していないなんてありえません!

たとえば、TODO項目としてBuy **beer**と[snyk](https://snyk.io/)を入力すると、次のように太字とハイパーリンクが表示されます。

ブラウザーの開発者ツールでHTMLリンクを調査している画面と、タスクが並ぶGoofのTODOリスト。「snyk」リンクが表示されている。

次に、悪意のあるペイロードを入力してみましょう。次のスクリーンショットは、上記3つの攻撃ペイロードをそれぞれ入力した後の表示とDOMの状態です。TODOリストなので、最初に入力した項目がリストの最後(3番目)に表示されている点にご注意ください。

ご覧のとおり、最初に試した2つの攻撃に対応する下の2項目は、サニタイザーによって<p></p>と<p>)</p>に変換されました。しかし一番上のペイロードは、クリックするとjavascript:this;alert(1)を実行するハイパーリンクの生成に成功しています。thisの実行自体は既存の変数を参照するだけで何も起こりませんが、alertによってポップアップが表示されます。

エクスプロイトのalertを少し分かりやすくしてリンクをクリックすると、次のようになります。

Goof TODOページに「marked exploit successful」と表示されたブラウザーのアラート。[OK]ボタンが表示されている。

Goofをローカルにインストールし、exploitsディレクトリにあるエクスプロイトのペイロードを使えば、この攻撃の流れを実際に試せます。

修正方法

少し珍しいことに、この問題を修正する公式のmarkedバージョンはありません。markedのリポジトリは昨年の夏以降更新されておらず、この脆弱性が公表されたのはそれより後でした。

ただし、SnykのWizardを使ってパッチを適用すれば、簡単に修正できます。このパッチは当社のセキュリティリサーチチームが作成し、Matt Austinがリポジトリに送った最初のプルリクエストを基にしています。

ほかのSnykのパッチと同様に、オープンソースの脆弱性データベースで詳細なパッチファイルを確認できます。実際には、markedのバージョンごとに3種類のパッチがあり、最もシンプルなものは次のとおりです。

10進数、16進数、名前付きHTMLエンティティに一致するよう修正された正規表現を示すコード差分。
  • 更新:markedのメンテナーは最終的に、2016年7月30日に新バージョンmarked(v0.3.6)を公開し、この問題に対処しました。

代替案として、markdown-itやremarkableなど、別のMarkdownパッケージを使うことも検討できます。脆弱性がない保証はありませんが、現時点で未修正の既知の脆弱性はありません。

新たに公表された、古い脆弱性

この脆弱性について、最後にもう1つ興味深い点があります。実は、かなり前から存在していました。問題は2015年5月に報告されましたが、脆弱性データベースに登録されたのは先月のことです。GitHub上の課題は膨大で、すべてを把握するのが難しいため、このような遅れはそれほど珍しくありません。

npmパッケージで報告されたセキュリティ問題を見つけた場合は、修正済みかどうかにかかわらず、security@snyk.ioまでお知らせください。確認のうえ、当社の脆弱性データベースに追加します。npmエコシステムの規模は非常に大きく、こうした問題を把握して安全を維持するには、皆で協力する必要があります。

CTFを始めよう

オンデマンドのバーチャル入門ワークショップで、CTFチャレンジの解き方を学びましょう。