コメント欄、プロフィール表示、検索結果の表示など、「ユーザーが入力した文字をそのまま画面に出す」機能は、Webアプリにはほぼ必ず存在します。
このとき、入力された文字を無防備にHTMLへ流し込んでしまうと、そこにスクリプトが仕込まれ、他の利用者のブラウザで実行されてしまいます。これがXSS(クロスサイトスクリプティング)です。
セキュリティ対策を横断的に確認したい方は、 Webアプリの脆弱性・セキュリティ対策まとめ もご覧ください。
例えば、コメント投稿機能を考えます。ユーザーが入力した文字列を、そのまま次のようにHTMLへ埋め込んでいたとします。
<div>入力された文字列</div>
ここに次のような文字列が入力されたらどうなるでしょうか。
<script>alert('XSS')</script>
ブラウザはHTMLとしてこれを解釈するため、<script>タグの中身をそのまま実行してしまいます。今回はalertですが、実際の攻撃ではCookieを盗み取ってログインセッションを乗っ取る、ページの内容を書き換えて偽の入力フォームを表示するといった悪用がされます。
<script>タグを禁止しているつもりでも、<img src="x" onerror="...">のように、タグの属性からスクリプトを実行する方法もあるため、「特定のタグだけ弾く」という対策では不十分です。
実際に手を動かして確かめてみましょう。以下のデモの「エスケープなし」側は、安全のため親ページから隔離された枠(iframe)の中だけで動作します。この記事のページ自体やあなたのCookieに影響することはありません。
左側は安全のため、隔離された枠(iframe・親ページのCookieや情報には一切アクセスできません)の中だけで実行されます。実際にタグを含む文字を入力して、表示のされ方の違いを確認してください。
「エスケープなし」側は、<script>やonerrorがそのまま実行され、意図しない挙動を起こしていたはずです。一方「エスケープあり」側は、タグがタグとして解釈されず、文字そのまま(<script>のような形)に変換されて画面に表示されています。これがエスケープの効果です。
XSS対策の基本は、ユーザー入力をHTMLとして出力する直前に、必ずエスケープ処理を行うことです。
エスケープとは、HTMLとして特別な意味を持つ文字(< > & " 'など)を、意味を持たない文字表現(<など)に置き換える処理です。
< → <> → >& → &" → "' → '
多くのテンプレートエンジン(Django テンプレート、JinjaなどのPython系、Reactなど)は、デフォルトで自動的にエスケープを行う設計になっています。危険なのは、この自動エスケープを意図的に無効化する機能(Djangoの|safeフィルタ、ReactのdangerouslySetInnerHTMLなど)を、必要のない場面で使ってしまうケースです。
エスケープと混同されやすい概念に「サニタイズ」があります。サニタイズは、危険な要素(タグやスクリプト)そのものを取り除く処理で、リッチテキストエディタのように「一部のHTMLタグだけは許可したい」場面で使われます。
一方エスケープは、タグを取り除くのではなく「タグとして解釈させない」処理です。単なるテキストとして表示したいだけなら、基本的にはエスケープだけで十分です。両者の違いについては、 入力バリデーション・サニタイズ・エスケープの違い でも整理しています。
HttpOnly属性を付け、スクリプトからのCookie読み取りを防ぐXSSは、ユーザー入力をそのままHTMLとして出力してしまうことで発生します。対策の基本は出力時のエスケープであり、多くのフレームワークではデフォルトで有効になっています。
まずは自分のアプリの中で「自動エスケープを無効化している箇所」がないかを見直すところから始めてみてください。