はじめに
セキュリティアドバイザリを見ると、反射型や蓄積型のXSS(クロスサイトスクリプティング)は今でも頻繁に報告されています。しかし、DOMベースXSSに関しては、近年その報告数が減少傾向にあります。
これはDOMベースXSSがブラウザ内のみで完結し、サーバーとの通信を必要としないという特殊な性質を持っているためです。かつては静的なHTMLページでも簡単に実証できましたが、現代のブラウザはセキュリティが強化されており、攻撃の成立は非常に難しくなっています。
エンジニアが知っておくべき基礎知識を分かりやすく解説します。
1. DOMベースXSSとは何か?
DOMベースXSSとは、ブラウザ上で動作するJavaScriptが、ユーザー入力などの「信頼できないデータ」を安全ではない方法で処理し、DOM(Document Object Model)を書き換えることで発生する脆弱性です。
最大の特徴は、「サーバーサイドの処理を経由せず、ブラウザ(クライアントサイド)だけで攻撃が完結する」 という点にあります。
従来のXSSとの決定的な違い
- 反射型・蓄積型XSS: 攻撃コードが一度サーバーに送られ、サーバーが生成するHTMLに悪意のあるスクリプトが混入します。
-
DOMベースXSS: サーバーは関与しません。URLのフラグメント(
#以降)など、サーバーに送信されないデータを使ってブラウザ内で実行されます。
2. 攻撃のメカニズム:「ソース」と「シンク」
DOMベースXSSを理解する上で重要なのが、Source(ソース) とSink(シンク) という考え方です。
-
Source (入力元): 攻撃者が操作できるJavaScript上のデータ。
-
location.hash(URLの#以降) -
location.search(URLのパラメータ) -
document.referrer(遷移元のURL)
-
-
Sink (実行先): 渡されたデータを「実行」したり「HTMLとして描画」したりする危険な関数・プロパティ。
element.innerHTMLeval()document.write()
「ソースから入った悪意あるデータが、無害化されずにシンクへ到達したとき」 に攻撃が成立します。
3. 具体的な攻撃例
脆弱なコードの例
以下は、URLのハッシュから取得した名前を画面に表示するだけの単純なコードです。
// URL: https://example.com/#Alice
const name = decodeURIComponent(window.location.hash.substring(1));
document.getElementById('display').innerHTML = "こんにちは、" + name + "さん";
攻撃シナリオ
攻撃者は、ターゲットに以下のようなURLを踏ませます。
https://example.com/#<img src=x onerror=alert('XSS')>
- ユーザーがこのURLにアクセスする。
- スクリプトが
#以降の<img>タグを読み込む。 -
innerHTMLによってHTMLとして展開される。 - 画像の読み込みエラーが発生し、
onerror属性内の JavaScriptが実行される。
これにより、Cookieの盗難や、偽のログイン画面への誘導といった被害が発生します。
4. DOMベースXSSを防ぐ3つの鉄則
① 危険なプロパティ(innerHTML)を使わない
最も効果的な対策は、HTMLとして解析を行わないプロパティを使用することです。
-
× 悪い例:
element.innerHTML = input; -
○ 良い例:
element.textContent = input;またはelement.innerText = input;
textContent を使えば、入力されたコードはただの「文字列」として表示されるため、スクリプトが動くことはありません。
② JavaScriptライブラリによるサニタイズ
どうしてもHTML構造を維持したまま動的に描画したい場合は、DOMPurify などの実績あるサニタイズライブラリを使用してください。
// 安全にHTMLをクリーンアップ
const cleanHTML = DOMPurify.sanitize(dirtyInput);
element.innerHTML = cleanHTML;
③ 危険な関数を避ける
文字列をそのままJavaScriptとして実行する eval()、setTimeout()、setInterval() にユーザー入力を渡すことは絶対に避けてください。
まとめ
DOMベースXSSは、サーバーサイドのセキュリティ対策(WAFなど)をすり抜けてしまうため、フロントエンドエンジニアの意識が非常に重要になります。
- 「ユーザーが操作できる値(Source)」を信用しない。
- 「HTMLを描画・実行する機能(Sink)」には慎重になる。
この2点を徹底して、安全なJavaScript実装を心がけましょう。