DOM型XSSの深淵:サーバーログに残らない「見えない攻撃」をどう防ぐか
やあ、現場の諸君。今日もフロントエンドの華やかな機能実装に追われているかな?
アプリケーションセキュリティの現場で、「WAFを導入しているからXSSは完璧」と豪語するエンジニアに出会うと、私は少しだけ眉をひそめる。なぜなら、DOM型XSSは、サーバーのログを一切汚さずにブラウザという「クライアントの庭」で完結する、極めて狡猾な攻撃手法だからだ。
今日は、サーバーサイドの防御網をすり抜け、ユーザーのセッションを奪い去る「DOM型XSS」のメカニズムと、明日から使える泥臭い防御策について、現場の知見を共有しよう。
—
1. DOM型XSSの本質:なぜサーバーを守っても防げないのか
反射型や格納型XSSは、攻撃ペイロードが一度サーバーを経由する。つまり、WAFやサーバー側のエスケープ処理で検知・無害化が可能だ。
しかし、DOM型XSSは違う。
攻撃の起点(ソース)はURLのフラグメント(#以降)やブラウザ内のデータであり、そこからJavaScriptがデータを読み取り、そのまま危険な関数(シンク)へ流し込むことで成立する。
攻撃のフローはこうだ:
1. 攻撃者が細工したURLを生成する(例: example.com/#)
2. ユーザーがそのリンクを踏む。
3. サーバー側では「#以降」は送信されないため、アクセスログには何も残らない。
4. クライアントサイドのJSがlocation.hashを読み取り、innerHTMLに流し込む。
5. ブラウザ上でJavaScriptが発火する。
サーバーサイドのエンジニアがどんなに堅牢なバリデーションを書いても、この「ブラウザ内部で完結するデータフロー」の前では無力なんだ。
—
2. 危険なソースとシンク(ここをマークせよ)
コードレビューを行う際は、以下の「ソース」から入力が入り、「シンク」に渡る経路がないかを血眼になって探してほしい。
- ソース(データの入り口):
location.search(クエリパラメータ)location.hash(フラグメント識別子)document.referrerwindow.name- シンク(実行される危険な関数):
innerHTML,outerHTMLdocument.write()eval(),setTimeout(string),setInterval(string)
特に、モダンなフロントエンドフレームワークを使っていても、v-html (Vue) や dangerouslySetInnerHTML (React) を安易に使えば、DOM型XSSの扉は簡単に開く。
—
3. 実践:セキュアな実装コード
「バリデーションさえしていれば大丈夫」という考えは捨てよう。「そもそも危険な関数を使わない」、あるいは「データをDOMに挿入する際に必ず無害化する」ことが鉄則だ。
悪い例:脆弱な実装
// URLのハッシュ値をそのままinnerHTMLに代入している(最悪のケース)
const hash = window.location.hash.substring(1);
document.getElementById(‘display’).innerHTML = hash;
良い例:DOMPurifyを活用した安全な実装
外部ライブラリの DOMPurify を使うのが、今の業界標準だ。これを通せば、スクリプトタグや危険な属性は徹底的に除去される。
import DOMPurify from ‘dompurify’;
// 1. ソースを取得
const hash = window.location.hash.substring(1);
// 2. 信頼できる関数で無害化してからシンクへ渡す
const cleanHTML = DOMPurify.sanitize(hash);
// 3. 安全に描画
document.getElementById(‘display’).innerHTML = cleanHTML;
※ もしライブラリを使えない場合は、innerHTMLではなく、必ず textContent を使うこと。これならHTMLタグとして解釈されず、ただの文字列として扱われるため、XSSは物理的に発生しない。
—
4. 最後の砦:Content Security Policy (CSP) の設定
アプリケーションコードの修正は必須だが、運用側として「多層防御」を敷くのもプロの仕事だ。Nginxの設定で Content-Security-Policy ヘッダーを吐き出しておこう。
Nginx設定ファイル例
スクリプトの実行元を制限し、インラインスクリプトを禁止する
add_header Content-Security-Policy “default-src ‘self’; script-src ‘self’; object-src ‘none’; base-uri ‘self’;” always;
この設定の意味:
script-src 'self': 自分のドメイン以外からのスクリプト読み込みを許可しない。'unsafe-inline'を含めない: これにより、HTML内に直書きされたタグや、onerror属性による攻撃が実行されなくなる。
---
最後に:エンジニアへのアドバイス
DOM型XSSは、コードの量が増えるほど見つけるのが困難になる。しかし、「外部からの入力を、変換せずにHTMLとして解釈させてはいけない」というたった一つの原則を守るだけで、攻撃のほとんどは未然に防げる。
「動けばいい」という実装から、「なぜこのコードは攻撃に強いのか」を説明できる実装へ。君たちが書くその一行が、ユーザーのセキュリティを守る最後の砦になる。
インシデントが起きてから対処するのは、二流の仕事だ。我々は、コードをコミットした瞬間にリスクを消し去る、一流のエンジニアであろうじゃないか。
では、また現場で会おう。健闘を祈る。
コメント