メインコンテンツまでスキップ

XSSについてもう少し詳しく

前の記事で、状況に応じて攻撃者はXSSペイロードを調整する必要があると述べました。たとえば、インジェクションポイントがinnerHTMLの場合、<script>alert(1)</script>は効果がありません。したがって、どのような攻撃方法があるのかを知るために、XSSについてもう少し詳しく知る必要があります。

攻撃を学ぶことは、防御を学ぶことです。まず攻撃方法を知らなければ、防御方法を知ることはできず、徹底的かつ効率的に防御することはできません。

JavaScriptを実行できる方法

HTMLを把握できるようになると、JavaScriptを実行する方法はたくさんあります。

最も一般的なのは<script>タグですが、その欠点の1つは、WAF(Web Application Firewall)、つまりウェブサイトで使用されるファイアウォールによって簡単に識別されることです。2つ目は、前の記事で述べたように、innerHTMLの状況では機能しないことです。

<script>以外にも、他のタグとインラインイベントハンドラを組み合わせてコードを実行できます。たとえば、次のようになります。

<img src="not_exist" onerror="alert(1)">

存在しない画像をロードし、onerrorを使用してコードを実行します。

実際、多くの人(私も含めて)は、srcをxと書きます。なぜなら、書きやすく覚えやすいからです。そして通常、xというパスは存在しませんが、存在する場合、onerrorはトリガーされません。したがって、ウェブサイトのルートディレクトリにxという名前の画像を置くと、一部の攻撃者はウェブサイトにXSSの脆弱性があることに気付かないかもしれないというジョークがあります。

onerror以外にも、イベントハンドラであれば何でも利用できます。たとえば、次のようになります。

<button onclick="alert(1)">Click me</button>

ボタンをクリックするとアラートが表示されます。ただし、この違いは、「ユーザーが何かアクションを実行する必要がある」ということです。たとえば、ボタンをクリックするなどです。一方、前のimgの例では、ユーザーは何もする必要がなく、XSSがトリガーされます。

もっと短くしたい場合は、svgonloadイベントを使用できます。

<svg onload="alert(1)">

ここで少し豆知識を補足します。HTMLでは、属性の"は必須ではありません。コンテンツにスペースがない場合は、基本的に削除しても問題ありません。タグと属性の間のスペースでさえ/で置き換えることができます。したがって、svgのペイロードは次のように書くことができます。

<svg/onload=alert(1)>

スペースも二重引用符も単一引用符も必要なく、XSSペイロードを作成できます。

一般的に利用されるイベントハンドラは次のとおりです。

  1. onerror
  2. onload
  3. onfocus
  4. onblur
  5. onanimationend
  6. onclick
  7. onmouseenter

onで始まるこれらのイベントハンドラ以外にも、コードを実行する方法があります。フロントエンドを書いたことがある人は見たことがあるかもしれません。

<a href=javascript:void(0)>Link</a>

これは、要素をクリックしても何も反応しないようにするためです。この例から、hrefを使用してコードを実行できることもわかります。たとえば、次のようになります。

<a href=javascript:alert(1)>Link</a>

まとめると、HTMLでJavaScriptを実行したい場合は、基本的に次の方法があります。

  1. <script>タグ
  2. 属性内のイベントハンドラ(すべてonで始まります)
  3. javascript:疑似スキーム

これらの方法を知っていれば、さまざまな状況に合わせて使用できます。

より多くのペイロードを知りたい場合は、Cross-site scripting (XSS) cheat sheetを参照してください。そこにはさまざまなペイロードがあります。

さまざまな状況でのXSSとその防御方法

通常、ペイロードを挿入できる場所を「インジェクションポイント」と呼びます。次のコードの場合:

<div>
Hello, <span id="name"></span>
</div>
<script>
const qs = new URLSearchParams(window.location.search)
const name = qs.get('name')
document.querySelector('#name').innerHTML = name
</script>

インジェクションポイントはdocument.querySelector('#name').innerHTML = nameの行です。

インジェクションポイントが異なると、攻撃方法と防御方法に影響します。以下に、3つの異なる状況を簡単に分類します。

HTMLへのインジェクション

これは最も一般的な状況です。上記の例も、以下のPHPも同様です。

<?php
echo "Hello, <h1>" . $_GET['name'] . '</h1>';
?>

これらの2つの例はどちらも、操作するための空白のHTMLを直接提供するため、任意の要素を自由に書き込むことができます。

たとえば、非常に一般的なペイロードである<img src=not_exist onerror=alert(1)>を使用すると、JavaScriptを実行できます。

防御方法は、ユーザー入力の<>をすべて置き換えることです。これにより、新しいHTMLタグを挿入できなくなり、何もできなくなります。

属性へのインジェクション

次のようなコードが表示されることがあります。入力内容は属性の値として使用され、属性内にラップされています。

<div id="content"></div>
<script>
const qs = new URLSearchParams(window.location.search)
const clazz = qs.get('clazz')
document.querySelector('#content').innerHTML = `
<div class="${clazz}">
Demo
</div>
`
</script>

この場合、上記の<img src=not_exist onerror=alert(1)>を使用しても機能しません。なぜなら、入力値は属性のコンテンツだからです。

XSSを実行したい場合は、まずこの属性をエスケープしてタグを閉じます。たとえば、"><img src=not_exist onerror=alert(1)>のようにします。そうすると、HTML全体は次のようになります。

<div class=""><img src=not_exist onerror=alert(1)>">
Demo
</div>

属性をエスケープすると、目的のHTMLタグを挿入できます。

この例から、状況が重要である理由がわかります。XSSがすべて最初の状況であると想定し、<>の2文字のみを処理した場合、この状況では失敗します。なぜなら、攻撃者は新しいタグを使用せずに攻撃できるからです。

たとえば、<>をまったく含まないペイロード" tabindex=1 onfocus="alert(1)" x="を使用すると、HTMLは次のようになります。

<div class="" tabindex=1 onfocus="alert(1)" x="">
Demo
</div>

HTMLタグを追加するのとは異なり、この攻撃方法は元のdivタグのonfocusイベントを利用してXSSを実行します。したがって、フィルタリングを行う際には、<>に加えて、'"もエンコードする必要があります。

また、これが次のようなコードを書くのを避けるべき理由でもあります。

document.querySelector('#content').innerHTML = `
<div class=${clazz}>
Demo
</div>

上記の属性は"'で囲まれていません。したがって、<>"'などの文字をエンコードして保護したつもりでも、攻撃者はスペースを使用して他の属性を追加できます。

JavaScriptへのインジェクション

HTML以外にも、ユーザーの入力がJavaScript内に反映されることがあります。たとえば、次のようになります。

<script>
const name = "<?php echo $_GET['name'] ?>";
alert(name);
</script>

このコードだけを見ると、"をエンコードするだけで十分だと思う人もいるかもしれません。なぜなら、そうすれば文字列から抜け出すことができないからです。しかし、これは問題があります。なぜなら、</script>を使用してまずタグを閉じ、次に他のタグなどを挿入できるからです。

したがって、この状況でも、以前と同様に<>"'をすべてエンコードして、攻撃者が文字列からエスケープできないようにする必要があります。

しかし、それでも、入力に空行を追加すると、改行が原因でコード全体が実行できなくなり、SyntaxErrorが発生することに注意する必要があります。

では、このような場合はどうでしょうか。

<script>
const name = `
Hello,
<?php echo $_GET['name'] ?>
`;
alert(name);
</script>

この場合、${alert(1)}という方法でJavaScriptコードを挿入し、XSSを達成できます。フロントエンドエンジニアは一目見ただけで問題が発生することがわかりますが、すべてのエンジニアが気付くとは限りません。おそらく、この部分はバックエンドエンジニアによって書かれたものであり、書いたときは単に「同僚が複数行の文字列を使用する場合はこの記号を使用するように言った」と考え、その意味や潜在的な危険性に気付かなかったのかもしれません。

まとめ

この記事では、XSSについてもう少し詳しく知りました。JavaScriptを実行する方法と、状況が異なれば必要な保護も異なることを学びました。

しかし、<>"'をすべてエンコードすれば、必ず安全なのでしょうか?それは必ずしもそうではありません。

この記事では少し触れましたが、詳しく説明しなかった、よく見過ごされる状況があります。これについては、次の記事で詳しく説明します。

次の記事に進む前に、ブレインストーミングをしましょう。JavaScriptを実行するには基本的に3つの方法があると前述しました。

  1. <script>タグ
  2. 属性内のイベントハンドラ(すべてonで始まります)
  3. javascript:疑似スキーム

そして、最初のものがinnerHTML = '<script>alert(1)</script>'の場合、動作しません。

では、イベントハンドラもjavascript:疑似スキームも使用できず、インジェクションポイントがinnerHTML = dataの場合、他にスクリプトを実行する方法はあるでしょうか?考えてみてください。