XSS(クロスサイトスクリプティング)は、攻撃者が用意したスクリプトを、正規のWebサイトの一部としてユーザーのブラウザで実行させる脆弱性です。原因は、外部から入力された値をエスケープやサニタイズをせずにページへ埋め込むことにあります。対策の基本は「出力する場所に合わせてエスケープする」ことで、そのうえでサニタイズ、Trusted Types、CSPを重ねます。
XSSとは
MDNは、XSSを「攻撃者が対象のWebサイトのコンテキストで悪意のあるコードを実行できるようにする攻撃」と説明しています。スクリプトが正規サイトのオリジンで動くため、同一オリジンポリシーによる保護が効きません。その結果、ページの書き換え、Cookieやセッション情報の窃取、ユーザーになりすました操作などが可能になります。
OWASP Top 10:2025では、XSS(CWE-79)はA05「Injection」に含まれます。OWASPはXSSを「頻度は高いが影響は比較的小さい」問題として位置づけていますが、ログイン中のユーザーの権限で何でもできてしまうため、軽視はできません。最新のTop 10全体はOWASP Top 10の解説にまとめています。
XSSの3つの種類
| 種類 | 仕組み | 例 |
|---|---|---|
| 反射型 | URLなどに含まれた値を、サーバーがそのままレスポンスに埋め込む | 検索語をそのまま「◯◯の検索結果」と表示する |
| 格納型(持続型) | データベースに保存された悪意ある入力が、閲覧した全員に配信される | コメント欄や掲示板への書き込み |
| DOMベース | サーバーを経由せず、ブラウザ側のJavaScriptが入力をHTMLとして挿入する | location.search の値を innerHTML に代入する |
格納型は、ページを開いた人すべてが被害に遭うため特に危険です。DOMベースはサーバーのログに痕跡が残りにくく、フロントエンドのコードレビューで見つける必要があります。
攻撃が成立する仕組み
たとえば、検索語を表示するページで次のような処理をしていたとします。
// 危険な例:URLの値をそのままHTMLとして挿入している
const params = new URLSearchParams(location.search);
const q = params.get("q");
document.querySelector("#result").innerHTML = `${q} の検索結果`;
攻撃者が q に <img src=x onerror=...> のような値を入れたURLを用意し、ユーザーにクリックさせると、onerror に書かれたスクリプトがそのサイトのオリジンで実行されます。
反射型XSSの流れ
- 罠のURLを用意パラメータにスクリプトを含める
- ユーザーがクリックメールやSNS、別サイトのリンクから
- 正規サイトが値を埋め込むエスケープせずにHTMLとして出力
- ブラウザがスクリプトを実行正規サイトのオリジンで動く
- 被害情報の窃取、なりすまし操作、画面の改ざん
入力値が出力される場所(コンテキスト)によって、必要なエスケープが変わる点にも注意が必要です。MDNは、HTML要素の中、HTML属性の中、JavaScriptやCSSの中を区別しています。属性値を引用符で囲まないと、引用符をエスケープしていても onmouseover=... のような値で属性を追加されるおそれがあります。<script> や <style> の中に入力値を直接入れるのは、ほぼ常に危険です。
根本的な対策
IPAの「安全なウェブサイトの作り方」は、HTMLの入力を許可しない場合の根本的解決として、次の項目を挙げています。
IPAが示すXSSの根本的解決
| 対策 | 内容 |
|---|---|
| エスケープ処理 | 出力するすべての要素で特殊記号をHTMLエンティティに置き換える。属性値はダブルクォートで囲む |
| URLの制限 | 出力するURLは http:// か https:// で始まるものだけを許可する |
| scriptの動的生成の禁止 | <script> 要素の内容を外部入力から生成しない |
| 外部スタイルシートの取り込み禁止 | 任意のサイトのスタイルシートを読み込める仕様にしない |
| 文字コードの明示 | Content-Type ヘッダーに charset を必ず指定する |
実際のコードでは、自動エスケープのあるテンプレートエンジンやフレームワークを使うのが近道です。ReactのJSXは、{} で埋め込んだ値を自動でエスケープします。
// 安全な例:JSXは埋め込んだ値を自動でエスケープする
export function SearchResult({ query }) {
return <p>{query} の検索結果</p>;
}
素のJavaScriptでDOMを操作する場合は、HTMLとして解釈されない textContent を使います。
// 安全な例:文字列をテキストとして挿入する
const q = new URLSearchParams(location.search).get("q") ?? "";
document.querySelector("#result").textContent = `${q} の検索結果`;
リンク先URLに入力値を使う場合は、スキームを確認してから設定します。
// 安全な例:http / https 以外のURLは使わない
function safeUrl(input) {
try {
const url = new URL(input, location.origin);
return url.protocol === "http:" || url.protocol === "https:" ? url.href : "#";
} catch {
return "#";
}
}
link.href = safeUrl(userInput);
HTMLを表示したい場合のサニタイズ
ブログのコメントやリッチテキストのように、利用者が書いたHTMLを表示したい場合もあります。このときは、許可した要素だけを残すサニタイズを行います。MDNとOWASPは、信頼できるライブラリとしてDOMPurifyを挙げています。
import DOMPurify from "dompurify";
// 安全な例:危険な要素や属性を取り除いてから挿入する
container.innerHTML = DOMPurify.sanitize(userHtml);
Reactの dangerouslySetInnerHTML やDjangoの safe フィルターは、自動エスケープを無効にします。使う場合は、必ず事前にサニタイズしてください。
HTMLとして解釈されるAPI(要注意)
innerHTML、outerHTMLinsertAdjacentHTML()document.write()eval()- 文字列を渡す
setTimeout()、setInterval()
代わりに検討するもの
textContentでテキストとして挿入- フレームワークの自動エスケープ
- HTMLが必要ならDOMPurifyでサニタイズ
- 関数を渡す
setTimeout() - Trusted Typesで危険なAPIへの代入を強制チェック
IPAは、<script> などの文字列を削除するのではなく無害な文字列に置き換えることや、ブラックリスト方式だけに頼らないことも注意点として挙げています。削除すると、残った文字が組み合わさって新たな危険な文字列ができることがあるためです。
Trusted TypesとCSPで被害を抑える
エスケープ漏れは、どれだけ注意していても起こり得ます。ブラウザの機能を使って、万一のときの被害を抑えておきます。
Trusted Typesを有効にする
CSPで
require-trusted-types-for 'script'を指定すると、innerHTMLなどに通常の文字列を代入したときにTypeErrorが発生します。サニタイズ用のポリシーを通した値しか受け付けなくなるため、エスケープ漏れを実行時に検出できます。Trusted Types自体はサニタイズをしないので、ポリシーの中でDOMPurifyなどを呼びます。厳格なCSPを設定する
nonceやハッシュを使ったCSPを設定すると、想定したスクリプト以外は実行されなくなります。注入された
<script>要素、インラインのイベントハンドラー、javascript:URL、eval()などがブロックされます。CookieにHttpOnly属性を付ける
IPAは保険的対策として、Cookieに
HttpOnly属性を付けてスクリプトからの読み取りを防ぐことを挙げています。ただし、Cookieの窃取以外の被害は防げません。
Trusted TypesのポリシーはMDNで次のように紹介されています。
// Trusted Types のポリシーでサニタイズを強制する
const policy = trustedTypes.createPolicy("my-policy", {
createHTML: (input) => DOMPurify.sanitize(input),
});
element.innerHTML = policy.createHTML(userInput);
CSPはレスポンスヘッダーで指定します。nonceはリクエストごとに推測できない値を生成し、許可するスクリプトタグにだけ同じ値を付けます。
Content-Security-Policy: script-src 'nonce-ランダムな値'; require-trusted-types-for 'script'
<script nonce="ランダムな値" src="/app.js"></script>
よくある質問
入力時にチェックすればXSSは防げますか?
入力値のチェックだけでは不十分です。IPAは入力値の内容チェックを保険的対策と位置づけ、幅広い文字を許可する仕様では効果が限られるとしています。根本対策は、出力する場所に合わせたエスケープです。
Reactを使っていればXSSの心配はありませんか?
JSXで値を埋め込むと自動でエスケープされますが、dangerouslySetInnerHTML を使う場合や、href に利用者が入力したURLを入れる場合は別途対策が必要です。URLは http: か https: で始まるものだけを許可してください。
CSPを設定すればエスケープは不要ですか?
不要にはなりません。MDNもCSPを「予備の防御」と位置づけています。CSPに対応していない環境や設定の抜けもあるため、エスケープとサニタイズを基本にし、CSPは被害を抑える層として重ねます。
XSSとCSRFはどう違いますか?
XSSは正規サイト上で攻撃者のスクリプトを実行させる攻撃、CSRFは別サイトからユーザーに意図しないリクエストを送らせる攻撃です。XSSがあるとCSRFトークンも読み取られてしまうため、CSRF対策の前提としてもXSS対策が重要です。詳しくはCSRFの解説をご覧ください。
出典: MDN「クロスサイトスクリプティング (XSS)」、IPA「安全なウェブサイトの作り方 1.5 クロスサイト・スクリプティング」、OWASP Top 10:2025 Introduction、OWASP A05:2025 Injection、OWASP Cross-Site Request Forgery Prevention Cheat Sheet