セキュリティ・ID

XSSとは|エスケープしたのに javascript: が残っていた

XSSでは具体的に何が起きているのかエスケープ処理をすればすべて防げるのか自分のアプリのどこを確認すればいいのか

コメント欄に書いた文字が、そのまま画面に表示される。よくある機能です。ここで文字ではなくHTMLのタグを書かれると、それがタグとして動いてしまうことがあります。これがXSSです。

今回は同じ入力を3か所に埋めて試しました。本文・画像の説明文・リンク先の3つです。結果を先に言うと、エスケープしたはずのリンクに javascript: がそのまま残っていました。埋める場所によって必要な処理が違うからです。

この記事の要点

  • XSSは入力がHTMLやスクリプトとして解釈されることで起きる
  • 対策の基本は出力するときに変換すること。入力時のチェックだけでは足りない
  • 実測では、URLに埋めた javascript: はエスケープを通り抜けた
  • 必要な処理は埋める場所によって違う。本文・属性・URLで別々に考える

XSSでは何が起きているのか

起きているのは、書き込まれた文字がHTMLの一部として読まれることです。文字を表示する欄が、そのまま部品を追加できる欄になってしまいます。

XSSは、利用者が書いた内容を画面に出すときに、それが文字ではなくHTMLとして解釈されることで起きます。コメント欄や名前の表示など、入力をそのまま出す場所が対象になります。

普通のコメントでは何も起きない

「こんにちは」と書き込まれた場合、画面にはその文字が出るだけです。ここまでは意図した動きで、問題はありません。

タグを書かれると、部品として組み込まれる

ところが <img src=x onerror=alert(1)> のような文字列を書き込まれると、ブラウザはこれを画像の指定として読みます。画像の読み込みは失敗しますが、失敗したときに動く処理の部分が実行されます

この例では警告を出すだけですが、同じ場所で他の処理も書けます。ログイン状態を保つ情報を盗む、勝手に操作を実行する、といったことが他の利用者のブラウザ上で起こります。

OWASPは、変数をHTMLの中に安全に入れるには、その変数にHTMLの実体参照への変換を行うよう示しています。つまり<&lt; のような別の表記に置き換えて、タグとして読まれないようにします。

入力を出力する場所が3種類あり、必要な処理はそれぞれ違う。同じ変換で全部は守れない。

HTML本文(必要な処理1つ)L3属性値(必要な処理2つ)L2リンク先URL(必要な処理2つ)L1依存の向きURLだけ性質が違う。変換は文字を無害にする処理で、URLの危険は文字ではなく行き先にあるため。
図1 ── 入力を埋める場所と、それぞれに必要な処理
余談 攻撃されるのは自分ではなく、他の利用者

XSSで被害を受けるのは、書き込んだ本人ではなくそのページを見た他の利用者です。ここが分かりにくい点だと思います。掲示板に仕掛けを書き込んでおき、それを見た人のブラウザで動く、という順序になります。

出典OWASP Cheat Sheet Series「Cross Site Scripting Prevention」2026-08-17 確認
In order to add a variable to a HTML context safely to a web template, use HTML entity encoding for that variable.
原文OWASP Cheat Sheet Series「Cross Site Scripting Prevention」 この内容の有効期限2027-02-17

エスケープしたのに、javascript: が残っていた

同じ入力を本文・属性・URLの3か所に埋めて試しました。本文と属性は変換で無害になりましたが、URLだけは通り抜けます。

XSSの対策は「エスケープすれば終わり」と説明されがちですが、実際に3か所へ埋めて出力を見比べました。

下のコードは読み飛ばして大丈夫です。やっていることは、同じ変換関数を3か所に使っただけです。

javascript
const escapeHtml = (s) =>
  String(s).replace(/[&<>"']/g, (c) => ({
    '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;'
  })[c]);

const renderSafe     = (v) => `<div class="comment">${escapeHtml(v)}</div>`;
const renderAttrSafe = (v) => `<img src="/avatar.png" alt="${escapeHtml(v)}">`;
const renderHrefEscaped = (v) => `<a href="${escapeHtml(v)}">プロフィール</a>`;

// URLはスキームを確かめる。変換では javascript: を止められない
const renderHrefSafe = (v) =>
  /^https?:\/\//i.test(v) ? `<a href="${escapeHtml(v)}">プロフィール</a>`
                          : '<a href="#">(不正なURL)</a>';
text
--- 入力をHTML本文に埋める ---
  対策なし: <div class="comment"><img src=x onerror=alert(1)></div>
  エスケープあり: <div class="comment">&lt;img src=x onerror=alert(1)&gt;</div>
--- 同じ入力を属性値に埋める ---
  エスケープあり: <img src="/avatar.png" alt="&lt;img src=x onerror=alert(1)&gt;">
--- URLとして javascript: を渡す ---
  エスケープのみ: <a href="javascript:alert(1)">プロフィール</a>
  スキームも確認: <a href="#">(不正なURL)</a>

残りました。href の中身が javascript:alert(1) のままです。エスケープ関数は通っているのに、変換すべき文字が1つも含まれていないので、何も変わらずに出てきました。

なぜURLだけ通り抜けるのか

変換が守っているのは「文字がタグとして読まれること」だけだからです。javascript:alert(1) には山括弧も引用符も入っていません。文字としては完全に無害で、危険なのは行き先そのものです。

つまりURLについては、文字を無害にする処理ではなく、行き先が許されるものかを確かめる処理が要ります。上のコードでは、先頭が http:// か https:// であることだけを条件にしています。

場所ごとに考える

OWASPのチートシートも、信頼できないデータをさまざまな場所で安全に描画する方法として、場所ごとに分けて示しています。1つの変換関数で全部を守る、という発想にならないよう作られています。

  1. HTML本文: 実体参照への変換を行う。多くのフレームワークは自動で行う
  2. 属性値: 変換に加えて、値を必ず引用符で囲む。囲まないと属性を足される
  3. リンク先URL: 先頭が http:// か https:// かを確認する。変換だけでは不十分
  4. HTMLをそのまま挿入する箇所: 専用の除去ライブラリを使う。自作しない
入口で止める設計にしない入力時にタグを禁止する方法もありますが、これだけに頼るのは危険です。データは他の経路からも入ってきますし、必要な処理は表示する場所によって変わります。出力する時点で変換するのが基本です。
出典OWASP Cheat Sheet Series「Cross Site Scripting Prevention」2026-08-17 確認
These snippets of HTML demonstrate how to render untrusted data safely in a variety of different contexts.
原文OWASP Cheat Sheet Series「Cross Site Scripting Prevention」 この内容の有効期限2027-02-17

自分のアプリのどこを確認すればいいのか

確認は2種類です。コメント欄にタグを入れて表示が崩れないか。URL欄に javascript: を入れて弾かれるか。この2つで大半が見つかります。

XSSの確認に特別な道具は要りません。実際に入力してみるのが確実です。ただし入れる値と見る場所を分けて考える必要があります。

確認1: 本文にタグを入れる

  1. コメント欄や名前の欄に <img src=x onerror=alert(1)> を入れて保存する
  2. 表示された画面で、文字としてそのまま見えるかを確かめる
  3. 壊れた画像の枠が出たり、警告が出たりしたら、その時点で穴がある

確認2: URL欄に javascript: を入れる

  1. プロフィールのURL欄など、リンクになる入力欄を探す
  2. javascript:alert(1) を入れて保存する
  3. 保存できてしまい、そのリンクを押して警告が出たら穴がある

この2つ目が、前の節で通り抜けたパターンです。本文の対策だけ済ませて安心している場合に見つかります

対策が済んだと言えるのは3条件すべて。URLのスキーム確認が最も抜けやすい。

充足 2 / 3本文への出力を変換している多くのフレームワークは自動で行うHTMLの直接挿入を使っていない使う場合は除去ライブラリが必要になるURLのスキームを確認している変換では javascript: を止められない3つ目が抜けやすい。本文の対策で満足してしまい、リンク先の検証を忘れる実装が多い。
図2 ── XSS対策が「済んだ」と言えるかの判定

防御ツールに任せきりにしない

通信を監視して攻撃を弾くしくみ(WAF)もありますが、OWASPはXSSの防止にWAFは勧められないと明記しています。特に、ブラウザ内だけで完結する種類のXSSには効きません。

入力を信用しないという点ではSQLインジェクションの記事と同じ構図です。あちらは入力がSQLの命令として読まれる問題で、こちらは入力がHTMLとして読まれる問題です。

余談 1回やって終わりにしない

この確認が効くのは実施した時点までです。入力を表示する場所は機能を足すたびに増えます。手作業の確認は最初の棚卸しに使い、以降はテストコードに任せるのが現実的です。タグを入れて出力を比べるだけなので、自動テストにも落としやすい部類です。

出典OWASP Cheat Sheet Series「Cross Site Scripting Prevention」2026-08-17 確認
WAFs are not recommended for preventing XSS, especially DOM-Based XSS.
原文OWASP Cheat Sheet Series「Cross Site Scripting Prevention」 この内容の有効期限2027-02-17

よくある質問

入力時にタグを禁止すれば防げませんか?
入口だけで防ぐのは難しい方法です。データは他の経路からも入ってきますし、表示する場所によって危険な文字は変わります。OWASPは出力する時点で変換することを基本に据えています。
フレームワークを使っていれば安全ですか?
多くのフレームワークは、本文に値を埋めるとき自動で変換します。ただし、HTMLをそのまま挿入する機能(dangerouslySetInnerHTMLなど)を使った箇所や、リンク先を組み立てる箇所は自分で守る必要があります。
リンク先はどう検証すればよいですか?
エスケープではなく、先頭が http:// か https:// であることを確かめる方法が確実です。javascript: や data: で始まるURLを弾けます。相対パスだけを許すなら、先頭が / であることを確認します。
Content-Security-Policyを入れれば十分ですか?
十分ではありません。CSPは被害を抑える保険であって、出力時の変換の代わりにはならないとOWASPは述べています。両方を行うものです。

まとめ

  • XSSは入力がHTMLとして解釈されることで起きる
  • 基本は出力時の変換。入力時のチェックだけに頼らない
  • 実測ではURLの javascript: がエスケープを通り抜けた
  • 埋める場所ごとに必要な処理が違う。本文・属性・URLで分けて考える

今日から始められること

  1. HTMLをそのまま挿入している箇所を全件洗い出す
  2. リンク先を入力から組み立てている箇所で、スキームを確認しているか調べる
  3. コメント欄に <img src=x onerror=alert(1)> を入れて、画像の壊れた表示が出ないか確かめる
  4. プロフィールのURL欄に javascript: で始まる値を入れて、保存できないことを確かめる

実務で組んだXSSのワークフローには、値段が付きます

同じ課題を持つ会社にとって、動いている設定は「作る時間」を買えるということです。ServiceDockは自作のワークフローやテンプレートを出品できるマーケットプレイスです。手数料や出品の流れは出品者向けページにまとまっています。

出品の仕組みを見る