やられアプリ BadTodo - 4.8 XSS 対策方法(エスケープ処理)

前回:やられアプリ BadTodo - 4.7 XSS(Todo詳細ページ) - demandosigno

やはり「安全なウェブサイトの作り方 - 1.5 クロスサイト・スクリプティング | 情報セキュリティ | IPA 独立行政法人 情報処理推進機構」の「対策について」が規範となります。

根本的解決(安全なウェブサイトの作り方より)

1.5.1 HTMLテキストの入力を許可しない場合の対策
* 5-(i) ウェブページに出力する全ての要素に対して、エスケープ処理を施す。
* 5-(ii) URLを出力するときは、「http://」や 「https://」で始まるURLのみを許可する。
* 5-(iii) 要素の内容を動的に生成しない。
* 5-(iv) スタイルシートを任意のサイトから取り込めるようにしない。
1.5.2 HTMLテキストの入力を許可する場合の対策
5-(vi) 入力されたHTMLテキストから構文解析木を作成し、スクリプトを含まない必要な要素のみを抽出する
1.5.3 全てのウェブアプリケーションに共通の対策
5-(viii) HTTPレスポンスヘッダのContent-Typeフィールドに文字コード(charset)を指定する。

安全なウェブサイトの作り方を読めばこの記事は必要ないくらいですが、BadTodoでの具体例に絞って書いていきます。

エスケープ処理

ブラウザは「<」のような特殊記号(メタ文字)をタグの開始と解釈します。一例としてHTMLで<s>タグを使うと打消し線が引かれます。打消し
これらの特殊記号はエスケープ処理することで<s>打消し</s>のようにただの文字列として扱われます。

変換前 → 変換後
& → &amp;
< → &lt;
> → &gt;
" → &quot;
' → &#39;

同様に<script>alert(1);</script>のようなスクリプトを&lt;script&gt;alert(1);&lt;/script&gt;と変換することで無害化できます。
ウェブページに出力する全ての要素に対して、エスケープ処理を施してください。

エスケープ処理はデータの入力時ではなく、HTML を出力する時に行わなければならないとされています。これを徹底しておけば、PHP スクリプトの内部で二重にエスケープ処理を行ってしまったり、エスケープを忘れたりするような問題を回避しやすくなります。
PHP と Web アプリケーションのセキュリティについてのメモ

htmlspecialchars 関数

BadTodoのようにPHPでアプリケーションを開発する場合には、HTMLのエスケープ処理にはhtmlspecialchars関数が利用できます。PHP: htmlspecialchars - Manual
使用例:echo htmlspecialchars($string, ENT_QUOTES, "UTF-8");
第2引数がENT_QUOTESの場合、変換対象となる文字は上で挙げた5つです(&, <, >, ", ')

例としてTodo「検索」時の挙動で検証します。
通常通りtestで検索したときのリクエスト https://todo.example.jp/todolist.php?rnd=64e6b73b4312a&key=test のHTMLレスポンスの当該部分は次の通り。

<form action='/todolist.php' method='get'>
<input type='hidden' name='rnd' value='64e6b73d3c321'>
  <input type="text" name="key" value="test"> ←ここ
  <input type="submit" value="検索">
  <label for="like-search">あいまい検索</label>
  <input type="checkbox" name="like-search" value="1" id="like-search" >
</form>

検索文字列を"><script>alert(1);</script>としたときは

<form action='/todolist.php' method='get'>
<input type='hidden' name='rnd' value='64e6b89dcc2c7'>
  <input type="text" name="key" value=""><script>alert(1);</script>"> ←ここ
  <input type="submit" value="検索">
  <label for="like-search">あいまい検索</label>
  <input type="checkbox" name="like-search" value="1" id="like-search" >
</form>

<script>alert(1);</script>がHTMLにそのまま出力されているためタグと解釈されXSSが発動します。

ソースコード上では次のようになっています。(todolist.php)

<?php $app->form($_SERVER['PHP_SELF'], false); ?>
  <input type="text" name="key" value="<?php echo $key; ?>"> ←ここ
  <input type="submit" value="検索">
  <label for="like-search">あいまい検索</label>
  <input type="checkbox" name="like-search" value="1" id="like-search" <?php echo $like_search ? ' checked' : ''?>>
</form>

ここの<?php echo $key; ?>の部分をhtmlspecialcharsを使って修正します。

<?php $app->form($_SERVER['PHP_SELF'], false); ?>
  <!--<input type="text" name="key" value="<?php echo $key; ?>">-->
  <input type="text" name="key" value="<?php echo htmlspecialchars($key, ENT_QUOTES, "UTF-8"); ?>"> ←ここ
  <input type="submit" value="検索">
  <label for="like-search">あいまい検索</label>
  <input type="checkbox" name="like-search" value="1" id="like-search" <?php echo $like_search ? ' checked' : ''?>>
</form>

するとXSSは発動しませんでした。

<form action='/todolist.php' method='get'>
<input type='hidden' name='rnd' value='64e6bde036efa'>
  <input type="text" name="key" value="&quot;&gt;&lt;script&gt;alert(1);&lt;/script&gt;"> ←ここ
  <input type="submit" value="検索">
  <label for="like-search">あいまい検索</label>
  <input type="checkbox" name="like-search" value="1" id="like-search" >
</form>

レスポンスの当該箇所が&quot;&gt;&lt;script&gt;alert(1);&lt;/script&gt;とエスケープ処理されたためです。

common.php にて

function e($s)
{
  echo htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
}

と定義されているため、

<input type="text" name="key" value="<?php e($key); ?>">

とすると少し楽です。

補足1

htmlspecialchars()の第2引数はENT_QUOTES以外にもあり、例えばENT_COMPATはダブルクォートは変換しますがシングルクォートは変換しません。
PHP: htmlspecialchars - Manual

ただそうすると
<input type='hidden' name='item' value='<?php echo htmlspecialchars($key, ENT_COMPAT, "UTF-8"); ?>'> のような属性値をシングルクォートで囲っているパターンにおいて' onclick=alert(1)などのシングルクォートしか使わないスクリプトを実行できてしまいます。
と書いた後で、hidden値ってクリックできなくないか?となりました。でも先人が何か良い方法を考えてくれているだろうとportswiggerのチートシートなどを確認してみるとありました。
Cross-Site Scripting (XSS) Cheat Sheet - 2024 Edition | Web Security Academy
Hidden inputs: Access key attributes can enable XSS on normally unexploitable elements
<input type="hidden" accesskey="X" onclick="alert(1)"> (Press ALT+SHIFT+X on Windows) (CTRL+ALT+X on OS X)

accesskey属性とFirefoxの組み合わせ。読み込んだ後、Windowsでは ALT+SHIFT+X キーを押すと発動する。
前回のBadTodo - 4.7 XSS Todo詳細ページがシングルクォートを使っているパターンですので試します。https://todo.example.jp/todo.php?item=2' accesskey='X' onclick='alert(1)
(Chromeの場合も<link><meta>その他の要素で動作するようです。<link rel="canonical" accesskey="X" onclick="alert(1)" />XSS in hidden input fields | PortSwigger Research

追記:徳丸さんも解説してくれていました。
「被害者にどうやって SHIFT+ALT+X を押させるか?」についても考察がありますので一読を。
hiddenなinput要素のXSSでJavaScript実行 | 徳丸浩の日記

追記2:ENT_COMPATでも> <はエスケープ処理されることを確認。
https://todo.example.jp/todo.php?item=2'><script>alert(1)</script>

補足2

また<input type="text" name="key" value=<?php echo htmlspecialchars($key, ENT_QUOTES, "UTF-8"); ?>>の value値のように属性値が引用符('")で囲まれていない場合
各記号をENT_QUOTESの方で処理しているので今度は大丈夫かと思いきや
1 onmouseover=alert(1)のようにシングルクォートも使わず半角スペースを空けるだけで属性を追加することができます。
(todolist.php の該当部分の引用符をわざと除いて試した例。上記文字列で検索リクエスト実行後に検索ボックスにマウスオーバーすると発動します)

というわけで属性値は引用符で囲んでください。ダブルクォートで良いかと思いますが実際にはシングルクォートで囲むことも多いためENT_QUOTESで統一して安全側に倒しておくことをお奨めします。

またフレームワークを使う場合はhtmlspecialchars関数は使わずテンプレートエンジンの機能で自動エスケープさせる方が良いです。

後日、保険的対応策についても検証します。

次回:やられアプリ BadTodo - 4.8 DOM Based XSS - demandosigno

/* -----codeの行番号----- */