HTTP・HTML・DOM・JavaScriptと入力検証・出力時エスケープ
Webアプリのプロフィール名に危険な文字列が登録されたとき、どの段階でただの文字列が実行可能なコードになるのでしょうか。架空のhttps://portal.exampleのプロフィール画面を使い、HTTPの往復、サーバの処理、ブラウザのHTML解析、DOM更新、JavaScript実行を順に追います。
科目B(午後)では『入力を検証する』と『出力をエスケープする』を混同せず、どの値が誰の支配下にあり、どの構文へ挿入されるか答えます。HTTP・HTML・DOM・JavaScriptの境界を知ると、XSSだけでなくログや構成図の読解も容易になります。ドメイン、ユーザ、ログ、コード断片は教材用の架空例です。
1. Web処理を構成する用語
用語 | 意味とこの例での役割 |
|---|---|
HTTPリクエスト | ブラウザからサーバへの要求。メソッド、対象パス、ヘッダ、必要なら本文を持つ。POST /api/profileで表示名を登録し、GET /profileで表示する。HTTPの意味と、アプリ固有の業務処理は別。 |
HTTPレスポンス | サーバからの応答。ステータス、ヘッダ、本文を持つ。GET /profileへの200応答の本文がHTMLなら、ブラウザはその文字列をHTMLとして解析する。200だけで画面の安全性や業務成功は証明できない。 |
HTML | 見出し、段落、フォーム、script要素などを記述するマークアップ。サーバが返すHTML文字列と、ブラウザが解析・変更した後のDOMは同一とは限らない。 |
DOM | ブラウザが文書をノードの木として表すモデル。JavaScriptはtextContentなどでテキストを入れ、innerHTMLなどでHTMLとして解析させられる。どちらを使うかで信頼できない文字列の意味が変わる。 |
JavaScriptの実行場所 | ブラウザのJavaScriptは利用者のページの文脈でDOMや通信を操作する。サーバのJavaScriptはサーバ上でDBやファイルへアクセスする。言語名が同じでも権限・見える資産・ログの場所が違う。 |
入力検証 | 受け付ける値の型、長さ、形式、範囲、業務条件をサーバで確かめる。displayNameが100文字以内かなどを判定する。文字種制限だけで全出力先の安全を保証しない。 |
出力時エスケープ | 保存した文字列を表示先の構文でデータとして扱わせる処理。HTMLテキスト、属性、JavaScript、URL、CSSなど出力先に応じて方法が違う。プロフィール名をHTML本文へ表示するならHTMLテキストとして符号化する。 |
信頼境界 | 利用者入力、DB保存値、外部API応答など、アプリが信頼してはいけない値がHTML生成・DOM更新・コマンド実行などの意味を持つ場所へ渡る境界。DBから読んだ値も入力元が利用者なら信頼済みにはならない。 |
2. HTTPの一往復から画面表示まで
利用者aliceが表示名を登録します。ブラウザはCookieでセッションを送り、サーバは認証・認可を確認したうえで本文を受け取ります。次のHTTP/1.1表示は理解用の例で、HTTP/2やHTTP/3ではワイヤ上の表現が異なります。
POST /api/profile HTTP/1.1
Host: portal.example
Cookie: session=<省略>
Content-Type: application/json
{"displayName":"<img src=x onerror=...>"}
HTTP/1.1 200 OK
Content-Type: application/json
{"result":"saved"}displayNameは利用者が支配する値です。サーバが登録に成功しても、まだブラウザで実行されたわけではありません。危険になるのは、後続のGET /profileでその値をHTML又は実行されるDOM操作へ安全な変換なしに入れたときです。ログにこの文字列があることも、ただちにXSS成立を示しません。
HTTPの往復、DB保存、HTML生成、DOM解析は別段階。
ブラウザは応答ヘッダと本文を受け取り、HTMLとして解析してDOMを作ります。その後、ページに含まれるスクリプトがDOMを更新する場合があります。サーバ側の入力検証と、HTML生成時・DOM更新時の扱いを別々に調べます。
2-1. リクエストとレスポンスのフィールド
フィールド | 読むときに分けること |
|---|---|
メソッドと対象 | GETは表現の取得、POSTは資源固有の処理を要求する。アプリがGETで状態変更を実装した場合、GETという名前が安全性を保証しない。パスとクエリはリクエスト対象で、本文とは別。 |
HostとOrigin | Hostは接続先ホストをHTTPで示す。Originはブラウザが送る場合の要求元オリジン。どちらもアプリでどう検証するか別問題で、Cookieやトークンの認可にも代わらない。 |
Cookie | ブラウザが該当する条件で送る状態情報。HTTP自体はステートレスでも、アプリはCookieのセッション識別子で利用者状態を扱える。Cookieの値をログへそのまま残さない。 |
Content-Type | 本文のメディア型を示す。application/jsonならJSONとして解析し、text/htmlならHTMLとして解釈する。ヘッダを偽装できる送信者に対しては、形式やサイズをサーバで確かめる。 |
ステータス | 200はそのHTTP要求の成功応答だが、アプリ内の全処理や後続のブラウザ実行まで保証しない。302は転送、4xxは要求側、5xxは処理側の問題を示す大分類。生成した機器も確認する。 |
レスポンス本文 | HTML、JSON、画像などが入る。HTMLに埋め込まれた保存値と、JSONを受けた後にJavaScriptがDOMへ入れる保存値では、危険になる場所が異なる。 |
3. HTML文字列、DOM、JavaScript実行を分ける
サーバ応答にdisplayNameをHTMLテキストとして入れる場合、値中の小なり記号などをHTMLの構文と解釈させないよう符号化します。ここでの例では、表示したい文字列そのものは変更せず、ブラウザがテキストノードとして読む形にします。
<!-- 危険: 信頼できない値をHTML構文へ直接連結 -->
<div class="name"><img src=x onerror=...></div>
<!-- HTMLテキストとして安全に表す例 -->
<div class="name"><img src=x onerror=...></div>
// JSONで受け取った値をDOMの文字列として入れる例
nameElement.textContent = profile.displayName;
// 値をHTMLとして解析させるため危険になり得る例
nameElement.innerHTML = profile.displayName;上のonerror=...は攻撃が成立する位置を示す省略表記です。実際のブラウザ挙動は要素、CSP、サニタイズなどにも依存します。重要なのは、最初の例では文字列がHTML要素・属性として解釈され、後二つでは同じ値がテキストになるかHTMLになるかがAPIで変わることです。
ブラウザ側スクリプトは通常、そのページのオリジンの権限でDOMや同一オリジンのAPIを利用できます。サーバ側スクリプトはブラウザDOMを直接持たず、サーバのDB資格情報や内部ネットワークへアクセスし得ます。『JavaScriptが動いた』だけでは、実行場所と利用できた資産を特定できません。
3-1. サーバ応答と現在のDOMはなぜ違うか
GET /profileのレスポンス本文には静的なHTMLがあっても、ブラウザは解析中にノードの木を作り、後からスクリプトがノードを追加・削除できます。開発者ツールのElementsで見る現在のDOMと、Networkで見る元のHTTPレスポンスを両方確認します。前者だけではサーバが送った文字列を、後者だけでは後続のDOM操作を確定できません。
観測対象 | 分かることと見落としやすいこと |
|---|---|
HTTPレスポンス本文 | サーバ又は中間キャッシュがブラウザへ渡したHTMLやJSON。scriptの後続実行で作られたDOMの全体像は含まない。 |
ブラウザのDOM | その時点でのノードの状態。初期HTML、テンプレート、スクリプト、拡張機能などの変更結果が混ざるため、原因の切り分けには変更履歴が要る。 |
JavaScriptのスタック・イベント | どの関数がinnerHTMLなどの出力先へ値を渡したかを追える。値の元がHTTP、localStorage、DB由来のJSONなどどこかを逆にたどる。 |
CSP違反レポート | ブラウザがCSPに反する実行などを検出した材料。違反がないことはXSS不存在を証明せず、拒否されたコードの意図や漏えいも単独では確定しない。 |
HTTPレスポンスがJSONでも、ブラウザがその文字列をinnerHTMLへ渡せばHTMLとして再解釈されます。逆にHTMLに埋め込む値を正しく符号化しても、後で別のスクリプトが同じ値を危険なAPIへ渡せば再び危険になります。出所から最終的な挿入先まで追う必要があります。
出力先 | 適した扱いと誤りやすい点 |
|---|---|
HTMLテキスト | テンプレートの既定のHTMLエスケープやDOMのtextContentを使う。<や&などがタグや実体参照として意味を持たないようにする。 |
HTML属性 | 引用符付き属性へ専用の属性エスケープを適用する。hrefやsrcでは構文の安全に加えてURLスキーム・宛先を検証する。属性エスケープだけでjavascript:のような危険なURLを許してよいわけではない。 |
JavaScript文字列 | データを直接scriptへ連結せず、安全なJSONシリアライズや外部データ取得を使う。HTMLテキスト用のエスケープをそのままJavaScript構文へ流用しない。 |
URLクエリ | 各パラメータ値をURL構文に合わせて符号化する。URLエンコードはHTML本文に置くときのエスケープの代わりにならない。 |
CSS・スタイル | 任意のCSS断片を連結せず、有限の許可値から選ぶ。HTMLテキストの変換でCSSの構文上の問題は解消しない。 |
DOMのinnerHTML | 値をHTMLとして解析する必要がなければtextContentへ変更する。必要なHTML断片なら信頼できるサニタイズを使い、許すタグ・属性・URLを限定する。 |
3-2. scriptの読み込み順は実行場所と別
HTML中の通常のclassic scriptは解析中に実行され、defer付きclassic scriptは解析完了後に実行されます。asyncは取得完了の時点で実行されるため、DOMの準備順に依存するコードで注意が必要です。moduleも依存モジュールの取得と評価順を持ちます。いずれも通常はブラウザ側の実行で、サーバ側JavaScriptに変わるわけではありません。
4. 入力検証は何を判断し、何を判断しないか
POST /api/profileではdisplayNameが文字列か、最大長、制御文字、業務上の禁止値をサーバで検査します。ブラウザ側のフォーム検証は操作性の助けになりますが、HTTP要求を直接送る攻撃者が迂回できるため、サーバ側の判断を省けません。
検証項目 | 適用位置と防げる範囲 |
|---|---|
型・長さ | JSONパーサ後にdisplayNameが文字列であり、文字数・保存サイズの上限内か確認する。巨大入力によるメモリ・DBの負荷も制限する。 |
形式・業務制約 | 表示名で許される文字や重複禁止を仕様に沿って決める。制約を過剰にすると正当な名前まで拒否するため、画面の用途と照合する。 |
認証・認可 | aliceが自分のプロフィールを変更できるか、他人のIDを指定していないか判定する。入力形式が正しくても、他人のデータ更新を許してはいけない。 |
出力時エスケープ | GET /profileのHTML生成やブラウザDOM更新の時点で、表示先の構文に合わせて行う。保存値が複数画面・メール・管理UIに出るなら各出力先を確認する。 |
たとえば<を禁止する入力規則でも、別のAPI、インポート機能、既存DB行を通じて危険な文字列が画面へ届く可能性があります。逆に記号を許す要件があるなら、記号を安全なテキストとして表示できる実装が必要です。入力検証は不正なデータや業務操作を拒否し、出力時エスケープは構文解釈を制御します。
4-1. 変換の順序と二重エスケープ
表示名をDBへ保存する時点でHTMLエスケープ済み文字列へ置き換えると、同じ値をJSON、メール本文、CSVへ出すときに扱いが複雑になります。原則として保存するのは検証済みの元データとし、表示する場所で必要な構文変換を行います。安全に文字を許すことと、どこへ出しても同じ変換で済むことは別です。
たとえばDBに<という5文字を保存した上でテンプレートもHTMLエスケープすると、画面には&lt;のように見える二重エスケープが起き得ます。反対に、表示が崩れるからとテンプレートの自動エスケープを全体で切ると、別の保存値からXSSが起き得ます。入力値、保存値、出力結果をそれぞれ記録して調べます。
処理段階 | 安全に扱うための判断 |
|---|---|
登録時 | 型、長さ、業務上の文字範囲と本人の変更権限をサーバで確認する。どの出力先でも使用できるデータとして保存する。 |
テンプレートでHTMLへ出す時 | テンプレートの自動エスケープを使い、HTMLとして挿入する例外を最小化する。例外箇所では許可するタグと属性を明示する。 |
JSONで返す時 | 正しいJSONシリアライズとContent-Typeを使う。JSONの文字列がブラウザで最終的にどのDOM APIへ渡るかを確認する。 |
ログやCSVへ出す時 | その出力先の構文と閲覧権限で考える。HTML用の変換を流用しない。機密値はログへ記録しない。 |
サーバでの入力検証を通った値でも、DB移行や管理画面から取り込まれた値は同じ検証を受けていないかもしれません。出力箇所の安全性を『過去に入力検証したはず』という前提に依存させないことが、保存型XSSに対する重要な設計です。
5. 架空ログで実行段階を切り分ける
以下は架空の一連のログです。時刻はUTCで、値を読みやすく短縮しています。request_idは説明用に同じ要求を結ぶIDです。ブラウザのDOM状態はサーバログに自動では残りません。
09:00:00 app request_id=A1 method=POST path=/api/profile
user=alice validation=pass saved_name=<img...>
09:01:00 app request_id=A2 method=GET path=/profile
user=alice status=200 template=profile-v2
09:01:01 browser trace=A2 dom_sink=innerHTML
input_source=/api/profile display_name=<img...>
effect=unexpected_event_handler観測 | 確定事項と限界 |
|---|---|
POSTのvalidation=pass | 形式・長さなど実装された検証を通り保存された。画面へ安全に表示されたことも、alice以外へ表示されたことも証明しない。 |
GET /profile 200 | サーバがHTML応答を返した。HTMLに保存値がどのように埋め込まれたかはレスポンス本文・テンプレートを確認する。 |
dom_sink=innerHTML | ブラウザ側コードが値をHTMLとして解釈するAPIへ渡した記録。実際の影響には実行環境・CSP・表示対象者・後続通信を調べる。 |
unexpected_event_handler | テスト環境で予期しないイベント処理が生じたという端末側観測。攻撃者への通信や情報持出しの証明には、ネットワークとサーバ側証跡が要る。 |
サーバ側では入力保存まで、ブラウザ側ではDOMへの挿入以降を追います。問題がサーバHTMLテンプレートにあるのか、フロントエンドのinnerHTMLにあるのかで修正箇所が異なります。HTTP 200やvalidation=passを『安全』の証拠にしません。
6. 攻撃条件と修正後の確認
- 1. 表示名を登録:証跡 POSTと保存行/防御・対処 型・長さ・権限を検証
- 2. 画面へ配布:証跡 HTML・JSON応答/防御・対処 出力先を確認
- 3. HTMLとして挿入:証跡 DOM操作と画面/防御・対処 textContentへ変更
- 4. 予期しない動作:証跡 端末・通信ログ/防御・対処 CSPと監視を併用
保存だけでは成立しない。被害者の画面に届き、危険な出力先で解釈される条件が要る。
攻撃者は表示名を書き込めること、被害者がその値を見ること、HTML又は危険なDOM APIへ値が到達することが必要です。入力欄に文字列を送っただけ、DBに保存しただけでは実行は確定しません。CSPは影響緩和に役立ちますが、脆弱なinnerHTMLを残す理由にはなりません。
保存値の出所と使用箇所を洗い出し、サーバHTMLテンプレートとDOM操作の両方を確認する。
HTMLテキストならテンプレートのエスケープ又はtextContentへ直す。属性、URL、スクリプトへ置く箇所は各構文に合った処理へ変える。
正常な日本語・記号入り表示名が表示でき、テスト用の危険な文字列が文字列として見えることを確認する。他の画面・管理UI・メールでも同じ保存値を試す。
攻撃期間中の表示対象者、端末側挙動、外向き通信を調べ、必要ならセッションや資格情報の失効を判断する。
回復を問われた場合は、修正コードの配備だけで終えません。保存済み値の表示経路がすべて修正されたか、既存データを安全に扱えるか、被害者のセッションと不審な操作が残っていないかを確認します。
6-1. アプリの修正と防御層の役割
CSPは、許可するスクリプト元やインライン実行を制約する追加の防御です。ただし設定次第で効果は変わり、危険なHTMLを生成する実装自体を正す必要があります。外部スクリプトを許している場合、その提供元が侵害されたときの影響も考えます。
CookieのHttpOnlyはJavaScriptからのCookie読取りを制限しますが、XSSがあれば同一オリジンのAPIへ被害者として要求を送れる場合があります。『Cookieを盗めないから影響なし』と結論しません。出力時の安全な扱い、CSP、セッション保護、アプリの認可をそれぞれの位置で確認します。
7. 科目B(午後)の記述演習
演習1:200と実行段階
条件:GET /profileが200で返り、DBにdisplayNameが保存された。問い:この二つの事実だけでXSSを断定できるか。
解答:断定できない。レスポンス内の出力方法、DOM操作、ブラウザでの解釈・実行を確認する。誤答『保存されたので必ず実行』は保存と構文解釈を混同する。
演習2:入力検証と出力
条件:表示名は100文字以内なら任意の記号を許す。プロフィール画面で値をテキストとして表示したい。問い:主な修正位置を述べよ。
解答:サーバでは型・長さ・認可を検証し、HTML生成時はHTMLテキストとしてエスケープする。DOM更新ならtextContentを使う。誤答『<を全面禁止するしかない』は正当な記号表示と出力時の安全な扱いを無視する。
演習3:サーバ側とブラウザ側
条件:/api/profileはJSONで値を返し、画面のJavaScriptがinnerHTMLへ代入した。問い:修正すべき場所を述べよ。
解答:ブラウザ側のDOM挿入をtextContent等へ直す。サーバ側の入力検証も維持する。誤答『JSONで返しているから安全』は受信後にHTMLとして再解釈される点を見落とす。
演習4:HTML属性への挿入
条件:表示名ではなく利用者指定URLをa要素のhrefに入れる。問い:HTMLテキスト用エスケープだけでよいか。
解答:足りない。属性として安全に挿入し、許可するURLスキームと宛先も確認する。誤答『<へ変えればすべての文脈で安全』はURLの意味と属性の構文を無視する。
演習5:元HTMLとDOMの差
条件:Networkのレスポンスには表示名が正しく符号化されているが、画面上では不正な要素が増えた。問い:次に調べる場所を述べよ。
解答:ブラウザ側で表示名を再取得し、innerHTMLなどへ渡すスクリプトと、その値の出所を追う。誤答『レスポンスが安全だからDOMも安全』は後続のDOM更新を見落とす。
演習6:サーバとブラウザのJavaScript
条件:ログにJavaScriptの例外があり、DB資格情報へのアクセスが疑われる。問い:実行場所をどう切り分けるか。
解答:サーバのプロセスログとブラウザの開発者ツール・スタックを分け、例外がどちらで起きたか確認する。ブラウザ側スクリプトは通常サーバのDB資格情報を直接持たない。誤答『JavaScriptなので必ずブラウザ側』は言語と実行環境を混同する。
8. 一次資料
RFC 9110:HTTPメソッド・ヘッダ・ステータスの意味
WHATWG HTML Standard:script要素と実行順
この記事についてAIに深掘り質問する
ChatGPT、Claude、Perplexityにこの記事を参照させ、要点の確認や疑問点を自由に質問できます。
次におすすめの学習
編集・検証について
編集・検証:IT資格ラボ編集部
IPAが公開する試験要綱・シラバス・過去問題と、各技術の公式資料を優先して内容を確認しています。制度変更や誤りを確認した場合は、記事を見直して更新します。
編集方針・情報源・訂正方針を見る