Webページで「文字」が命令に変わる危険
XSSとは?コメント欄の文字が『命令』に変わる仕組みと防ぎ方を図解
通販サイトのレビュー欄や検索結果には、誰かが入力した文字が表示されます。もし、その文字がブラウザに「HTMLやJavaScriptの命令」として読まれてしまったら?
1. コメント欄に書いた文字が動く?
ネットショップの商品レビューに「軽くて使いやすい!」というコメントが投稿されるのは普通のことです。サイトはその文章を保存し、商品ページを開いた人に表示します。
では、投稿者が文章の代わりにHTMLタグに見える文字を送っていたら? サイトがそれをただの文章として表示すれば問題ありません。しかし、サイトが「画面に埋め込むHTMLそのもの」として扱った場合、ブラウザが予想しない要素や処理を解釈する恐れがあります。
ここで使う例はすべて安全な模型です。実際に人の情報を盗むスクリプトや攻撃コードは必要ありません。
2. 『ただの文字』と『HTML』は何が違う?
たとえば、レビュー欄の入力が <strong>おすすめ</strong> だったとします。
文字として表示する
<strong>おすすめ</strong>
入力した山括弧も、そのまま読むことができます。
HTMLとして解釈する
おすすめ
タグは命令として扱われ、文字が太くなります。
太字にするだけなら無害ですが、HTMLには画像・リンク・イベント属性など様々な機能があります。入力した人がHTMLの構造まで決められると、意図しないスクリプト実行に結び付く可能性があります。
つまり、悪いのは「山括弧を入力できること」自体ではありません。受け取ったデータを、信頼できるHTMLやJavaScriptのように扱うことが問題です。
3. 3つのXSSを身近な例で追いかける
① Stored XSS:レビューに残り続けるタイプ
- 誰かがレビュー欄へ、HTMLの命令として解釈され得る文字列を投稿する。
- サイトがその入力をデータベースに保存する。
- 別の人が商品ページを開く。
- 保存された文字列が安全に処理されず、ブラウザで解釈される。
掲示板・プロフィール欄・商品レビューなど、保存したデータが繰り返し表示される場所で問題になります。投稿した本人がその場にいなくても影響が続き得る点が特徴です。
② Reflected XSS:検索結果に反射するタイプ
サイトが「『赤い靴』の検索結果」のように検索語を画面に表示するとします。URLの検索語が、そのまま危険な形で画面へ埋め込まれるなら、細工されたリンクを開いた人のブラウザで問題が起きる場合があります。データベースに保存される必要はありません。
③ DOM-based XSS:画面内のJavaScriptが原因となるタイプ
ページがURLの # 以降の文字を読み、JavaScriptで画面を書き換えるとします。そのとき、文字として表示すべき値をHTMLとして挿入すると、サーバのHTML生成を通らずに問題が発生し得ます。
| 種類 | 身近な入口 | 危険な処理が起こる場所 |
|---|---|---|
| Stored | レビュー・掲示板 | 保存された内容の表示時 |
| Reflected | 検索キーワード・URLパラメータ | レスポンスへの埋め込み時 |
| DOM-based | URLの#や画面内データ | ブラウザのJavaScriptでDOM更新時 |
4. XSSが起こると何が困る?
意図しないスクリプトが正規サイトの文脈で動けば、表示中のページを勝手に書き換えたり、偽のログイン欄を出したり、利用者に代わって画面上の操作を進めたりする可能性があります。
「Cookieを盗む攻撃」とだけ覚えるのは不正確です。Cookieに HttpOnly が付いていればJavaScriptから直接読み取れませんが、だからといってXSS自体がなくなるわけではありません。ページ内のデータ参照や、本人の権限を使った操作など、別のリスクが残ります。
ログインしていないページでも、偽情報の表示や詐欺的な誘導などに使われる可能性があります。XSSの目的は一つに限りません。
5. 防御①:入力を『文字』として表示する
いちばん基本的な対策は、外部から来た文字列を、HTMLの命令ではなくテキストとして出力することです。これを出力エスケープ(適切な出力エンコーディング)と呼びます。
// React / Next.js の通常のテキスト表示
function Review({ text }: { text: string }) {
return <p>{text}</p>; // テキストとして扱われる
}Reactの通常のテキスト埋め込みはHTML特殊文字を安全に扱います。たとえば < をHTMLのタグ開始として解釈させず、文字として表示します。
ただし、すべての場所で同じエスケープを使えばよいわけではありません。HTML本文、HTML属性、URL、JavaScript文字列、CSSでは、解釈の規則が違います。特にイベント属性やスクリプトそのものなど、危険な場所に信頼できない値を直接埋め込まない設計が重要です。
6. 防御②:太字などのHTMLを許す場合は?
ブログの本文やチャットの装飾機能など、利用者が「太字」や「リンク」を付けられるサービスもあります。この場合、すべてテキストにすると装飾ができません。
そこで使うのがHTMLサニタイズです。HTMLを解析して、安全と判断したタグ・属性だけを残す処理です。例えば太字を表す <strong> は許可しつつ、イベント属性など実行に結び付くものを取り除きます。
サニタイズは単純な正規表現や <script> の文字列置換だけで作らず、実績のあるHTMLサニタイザを利用します。リンク先URLのスキーム、SVGやMathML、ブラウザのHTML解釈なども考慮が必要だからです。
// 危険な書き方の例:投稿内容をHTMLとして解釈させる
<div dangerouslySetInnerHTML={{ __html: userHtml }} />
// HTML不要ならこちらが基本
<div>{userHtml}</div>dangerouslySetInnerHTML が必要な用途では、信頼できない値を直接渡さず、適切にサニタイズした結果だけを使ってください。サニタイズ後に別の文字列処理を加えると、安全性を壊すこともあります。
7. 防御③:CSPやCookieは何を守る?
Content Security Policy(CSP)は、どのスクリプトを読み込めるか、インラインスクリプトをどう扱うかなどをブラウザに伝える仕組みです。nonceやハッシュを使う厳格なポリシーを適切に組むことで、XSSの悪用を難しくできます。
ただしCSPは追加の防御層であり、脆弱な出力処理を放置してよい理由にはなりません。HttpOnly・Secure・SameSite などのCookie属性も被害軽減には役立ちますが、XSSの発生そのものを止めるものではありません。
また、Chromium系ではTrusted Typesにより、危険なDOM操作へ未検証の文字列を渡しにくくする方法もあります。チーム開発でDOM操作の安全性を高めるための補助策です。
8. 操作で理解する:文字と命令の境界
普通のコメント、HTML風の文字、イベント属性を含む文字列を選び、「テキスト表示」「生HTML扱い」「サニタイズ」の違いを比べましょう。危険な処理は一切動かさず、ブラウザがどう解釈するかを模式的に示します。
体験:文字として表示する? HTMLとして解釈する?
危険なJavaScriptは一切実行しない、安全な表示シミュレーターです。入力元と表示方法を変えて比べます。
入力データ(文字列)
<img src="broken" onerror="/* 想定外の処理 */">
element.textContent = userInput;
画面上での結果(実際にHTMLとして解釈せず安全に模式表示)
<img src="broken" onerror="/* 想定外の処理 */">
内部表現:<img src="broken" onerror="/* 想定外の処理 */">
HTMLの形をした文字列も、ブラウザへの命令ではなく文字として表示されます。Reactの通常のテキスト埋め込みもこの方向です。
分類:Stored XSSの入口。ただし入力元だけで分類は確定せず、どこでデータが危険な命令に変わるかが重要です。
9. CSRF・クリックジャッキングとの違い
| 攻撃 | よくある場面 | 防ぐ中心 |
|---|---|---|
| XSS | レビューの文字列が命令として解釈される | 安全な出力・DOM操作・サニタイズ |
| CSRF | ログイン中に意図しないリクエストが送られる | CSRFトークン・送信元検証 |
| クリックジャッキング | 見えないボタンが別のボタンの上に置かれる | frame-ancestorsで埋め込みを防ぐ |
CSRF対策に使うトークンも、同じサイト内でXSSが成立すると、そのページの権限や情報を悪用されることがあります。XSS対策とCSRF対策は別物ですが、互いに無関係ではありません。
10. 開発するときに気をつけたいこと
URLの値で画面を更新する場合
たとえば検索キーワードやURLのハッシュ値を画面に表示する場合、ブラウザ内JavaScriptでHTMLとして挿入しないようにします。
// 危険:外部入力をHTMLとして渡す
// panel.innerHTML = location.hash.slice(1);
// 安全な基本形:文字列として表示する
panel.textContent = location.hash.slice(1);Reactであれば、画面に表示する値を通常のJSXテキストとして扱うことが基本です。リンクや画像URLとして使うときは、危険なURLスキームを認めないなど、別途URLの検証が必要です。
チェックリスト
- 利用者入力・API応答・URLなどを、基本的に信頼しないデータとして扱う。
- 画面にはReactの通常のテキスト表示や
textContentを優先する。 - 生HTMLが必要なら、用途を限定して実績のあるサニタイザを使う。
- URL・属性・CSS・JavaScriptの文脈に適した処理を選ぶ。
- CSP・Cookie・Trusted Typesなどを追加の防御層として検討する。
- テンプレート、DOM操作、サードパーティー部品を含めてレビューする。
11. よくある疑問とまとめ
Q. 入力欄から「<」を禁止すれば十分?
十分ではありません。HTML本文以外の文脈で危険になることもあり、禁止文字の追加だけでは取りこぼしが起きます。安全に表示する仕組みを使うことが重要です。
Q. HTTPSならXSSは起きない?
HTTPSは通信を守りますが、受け取った文字列をブラウザがどう解釈するかまでは守りません。
Q. Reactなら絶対に安全?
通常のテキスト表示には強みがありますが、生HTMLの挿入、危険なDOM操作、URLの扱い、サードパーティーライブラリの実装などには注意が必要です。