ゆめさくセキュリティ

レッスン 2-2

クロスサイトスクリプティング

エスケープの場所を間違えると、対策は効きません。

利用者の入力をページに出すとき、値のはずの文字がページの部品 (タグや属性) として読まれる穴です。本文、属性、URL の 3 か所で、エスケープ (特別な記号を、ただの文字として表す置き換え) の仕方が違うことを、脆いコードと直したコードの並べ比べで確かめます。

  • 目安60 分
  • 出典を確かめた日2026年10月24日
  • 修了の条件エスケープの場所を間違えると対策が効かなくなる例を挙げられる

なぜ通るのか

会員の名前を表示するプロフィールの画面を作ったとします。サーバーは、登録された名前を HTML の文字列の中に埋め込んで、ブラウザに返します。

ここで名前をそのまま HTML につなぐと、ブラウザには「どこまでがページの部品で、どこからが利用者の文字か」の区別が届きません。ブラウザが受け取るのは、つなぎ終わった 1 本の HTML だけだからです。

クロスサイトスクリプティング (XSS) は、この区別の崩れを使って、攻め手の用意したスクリプトを、そのサイトのページの中で動かす穴です。動いたスクリプトは、そのサイトの一員として、ページの中身を読み、画面を書き換え、利用者に代わって操作を送れます。

文字とタグが混ざる所

HTML では、山括弧 (< と >) がタグの始まりと終わりを表します。名前に山括弧が入っていると、ブラウザはそこをタグとして読みます。

悪意が無くても起きます。たとえば名前に「<b>Ann</b>」と書いた人がいると、画面には太字の「Ann」が出て、山括弧は消えます。利用者の文字が、ページの部品に化けたということです。太字のタグに化けるなら、ほかのタグにも化けられます。

エスケープの場所

防ぐ基本は、出力するときのエスケープです。山括弧や引用符を &lt; や &quot; のような「文字としての書き方」に置き換えると、ブラウザはそれをタグではなく文字として表示します。

ただし、置き換える記号と方法は、値を置く場所で変わります。OWASP の Cheat Sheet は、HTML の本文、属性、JavaScript、CSS、URL の場所ごとに、別のエンコード (書き換えの決まり) を使うよう求めています。

置く場所要る処理
本文& < > " ' を文字の書き方に
属性の値引用符で囲み、引用符も置き換える
URL の中の値URL の決まりで書き換えてから、HTML のエスケープ

1 か所で正しい処理を、別の場所に流用すると効きません。このレッスンの中心はここです。

CSRF との違い

次のレッスンで扱うクロスサイトリクエストフォージェリ (CSRF) と、名前がよく似ています。被害の出方は、次のように違います。

XSSCSRF
何が起きるか攻め手のスクリプトがページの中で動くブラウザが本人の権限で要求を送らされる
攻め手に見える物ページの中身と応答を読める応答は読めない。送らせるだけ
止める所出力のエスケープ要求が本人の操作かの確かめ

XSS があると、ページの中のトークン (本人の操作の印) も読めます。OWASP の CSRF の Cheat Sheet は、XSS があれば CSRF の対策はすべて越えられると注意しています。

脆いコードと直したコード

ラボの中の小さな関数で比べます。Python に付いている html と urllib.parse だけで動き、外とは通信しません。どの例も、山括弧や引用符を含む、悪意の無い名前で崩れ方を見せます。

本文に置くとき

脆いコード名前をそのままつなぐ
def card(name):return "<p>" + name + "</p>"
直したコード本文のエスケープをかける
from html import escape def card(name):return "<p>" + escape(name) + "</p>"

脆い方に「<b>Ann</b>」を渡すと、b のタグが 1 つ増えます。直した方では &lt;b&gt; に置き換わり、画面には山括弧ごと文字として出ます。

属性に置くとき

脆いコード引用符を置き換えない
from html import escape def field(name):v = escape(name, quote=False)return ('<input value="'+ v + '">')
直したコード引用符も置き換える
from html import escape def field(name):v = escape(name)return ('<input value="'+ v + '">')

脆い方もエスケープはしています。山括弧は置き換わるので、本文なら足ります。ところが属性の値は引用符で囲まれているので、引用符を置き換えないと、値がそこで閉じます。

あだ名を「Ann "Bee" Lee」と書いた人で試すと、脆い方では value が「Ann 」で終わり、残りの「Bee"」と「Lee"」が別の属性として読まれます。属性を足せるなら、ふるまいを変える属性も足せます。直した方は引用符を &quot; に置き換えるので、値は 1 つのままです。

URL の中に置くとき

脆いコードHTML のエスケープだけ
from html import escape def link(name):url = "/users?name=" + namereturn '<a href="' + escape(url) + '">'
直したコードURL の決まりで先に書き換える
from html import escapefrom urllib.parse import quote def link(name):url = "/users?name=" + quote(name)return '<a href="' + escape(url) + '">'

「Tom & Jerry」で試します。脆い方では、& は &amp; に置き換わりますが、ブラウザは属性を読むときに &amp; を & に戻します。行き先の URL では name の値が「Tom 」で終わり、「 Jerry」は別の項目として読まれます。HTML のエスケープは HTML の決まりを守るだけで、URL の決まりは守りません。

直した方は、先に quote で URL の中の値として書き換え (%26 など)、そのうえで HTML のエスケープをかけます。外側の決まりが内側の決まりを守ってくれることはないので、内側から順にかけます。

もう 1 つ、利用者が入れた URL を丸ごとリンクにする場合は、エスケープでは足りません。IPA は、http と https で始まる URL だけを許すよう求めています。URL の始まりの方式 (スキーム) によっては、リンクを押しただけでスクリプトが動くからです。

ブラウザの中で書き換えるとき

サーバーではなく、ブラウザの JavaScript でページを書き換える所にも同じ穴が開きます。innerHTML に文字列を入れると、ブラウザはそれを HTML として読みます。OWASP は、文字を出すだけなら textContent を使うよう勧めています。textContent は HTML として読まず、文字のまま置きます。

ログでの見え方と検知

XSS は、サーバーの記録にエラーが残りにくい穴です。崩れが起きるのは利用者のブラウザの中だからです。そこで、ブラウザから知らせを受け取る仕組みを置きます。

CSP (Content Security Policy、ページで動かしてよいスクリプトの出どころをブラウザに伝える決まり) には、止めずに知らせるだけの Content-Security-Policy-Report-Only があります。まずこちらで出し、ふだんの利用で違反が出ないことを確かめてから、止める側の Content-Security-Policy へ移します。

記録知らせを受け取る口の記録の例
csp-report path=/profiledirective=script-srcblocked=inline count=3

この行は、許していないスクリプトがそのページに現れたことを示します。守る側は、次の 3 つを記録と見張りに入れます。

見る物なぜ効くか
CSP の違反の知らせの数ふだんの利用では、違反はほとんど出ない
違反が出たページと時刻どの画面のどの入力かを、あとから追える
コードの中の危ない書き方innerHTML や自動エスケープを切る書き方は、穴の候補

コードの見回りでは、テンプレートの自動エスケープを切る書き方 (たとえば |safe や Markup() と、innerHTML への代入を探します。

直し方と、直ったことの確かめ方

自分の PC の中だけで行います。外のサービスや、他人の機械には向けません。

  1. 作業用のフォルダに上の 3 つの関数を置き、名前「<b>Ann</b>」、あだ名「Ann "Bee" Lee」、名前「Tom & Jerry」を用意する
  2. 脆い関数の出力を Python の HTMLParser で読み、タグや属性や URL の値が増えることを確かめる
  3. 関数を直したコードに置き換える
  4. 同じ入力で、タグも属性も増えず、URL の name が元の名前に戻ることを確かめる
  5. 下の試験を足し、直す前のコードに戻すと試験が落ちることも確かめる
  6. 終わったら、作業用のフォルダごと消す
Python直ったことを固める試験
def test_body_stays_text():tags, text = parse(card("<b>Ann</b>"))assert [t for t, _ in tags] == ["p"]assert text == "<b>Ann</b>" def test_attr_stays_one_value():tags, _ = parse(field('Ann "Bee" Lee'))assert tags[0][1] == [("value", 'Ann "Bee" Lee')]

parse は、HTMLParser で出力を読み、タグと属性の一覧と、文字の部分を返す小さな関数です。試験は「同じ入力で、直す前はページの部品が増え、直したあとは文字のまま」を固定します。

あわせて、手で HTML をつなぐのをやめ、自動でエスケープするテンプレートや画面の部品に任せます。CISA と FBI は、出力のエンコードを簡単に使える現代的なフレームワークを使うよう求めています。セッションのクッキーには HttpOnly を付けます。IPA は、スクリプトからクッキーを読めなくするこの印を、保険的な対策に挙げています。

関わる事例

CISA と FBI は 2024年9月18日に、XSS をソフトウェアから無くすよう作り手に求める文書 (Secure by Design Alert) を出しています。XSS は防げる種類の穴で、ソフトウェアにあってはならないと述べています。入力の消毒 (危ない部分を取り除くこと) だけでは確実でないこと、フレームワークの出力のエンコードを使うこと、コードレビューを行うことを挙げています。

IPA の「安全なウェブサイトの作り方」は、ウェブページに出すすべての要素にエスケープをかけることを根本的な解決に挙げ、属性の値を必ず二重引用符で囲むよう書いています。このレッスンの本文と属性の並べ比べは、この 2 点に当たります。

確認クイズ

1 問目

入力の値を `<input value="...">` の属性に出します。山括弧だけを置き換えるエスケープをかけました。何が起きますか。

  • ア 山括弧を置き換えたので、どの入力でも安全
  • イ 引用符を含む入力で値が途中で閉じ、別の属性が足される
  • ウ 属性の中では、HTML のエスケープは要らない
答えと理由を見る

答え イ

属性の値は引用符で囲まれています。引用符を置き換えないと、入力の中の引用符で値が閉じ、その後ろが別の属性として読まれます。属性に置くときは、引用符も置き換え、値を必ず引用符で囲みます。

2 問目

検索語を `/search?q=` の後ろにつなぎ、リンクにします。正しい順はどれですか。

  • ア HTML のエスケープだけをかける
  • イ URL の決まりで書き換えだけをかける
  • ウ URL の決まりで書き換えてから、HTML のエスケープをかける
答えと理由を見る

答え ウ

ブラウザは属性を読むときに HTML のエスケープを元に戻すので、HTML のエスケープだけでは URL の中の `&` が区切りとして読まれます。URL の決まりで書き換えるだけでは、属性の引用符を守れません。内側の URL から順にかけます。

説明課題

次のことを、自分の言葉で 3 行にまとめてください。書けたら、このレッスンは修了です。

  • エスケープの場所を間違えると対策が効かなくなる例を 1 つ挙げ、なぜ効かないのか
模範解答を見る

URL の中の値に HTML のエスケープだけをかけると、& は &amp; になるが、ブラウザが属性を読むときに & へ戻す。戻った & は URL の区切りとして読まれ、値が割れる。URL の決まりで先に書き換え、そのあと HTML のエスケープをかけて、内側から順に守る。

一次資料

どれも 2026年10月24日に開いて、題と中身を確かめました。

次へ

次のレッスンは「クロスサイトリクエストフォージェリ」です。スクリプトを動かさなくても、ログイン済みのブラウザに本人の権限で要求を送らせる穴を扱います。

次のレッスン 2-3 クロスサイトリクエストフォージェリ

このレッスンにつながる物