Hello there, ('ω')ノ
ねらい
このLABは、商品ページの在庫チェッカーで使われているJavaScriptが、URLのlocation.search(クエリ文字列)から値を取り出し、そのままdocument.writeで<select>配下に書き込む、という危険な実装になっている点を突きます。攻撃者はstoreIdというURLパラメータを細工してselect要素から脱出し、任意のHTML要素+イベントハンドラを挿入することでalert(1)を実行します。
全体像(まずはストーリー)
- 商品ページのURLにstoreIdを付与すると、その値が<select>の<option>として画面に表示されることを確認。
- HTMLをどう書き換えているのか、document.write(シンク)がlocation.search(ソース)を直接使っていることを読み解く。
- 文脈は<select>内、かつ<option value="...">の属性値コンテキストなので、まず属性を閉じるためのダブルクォートとタグ終端の “>”で脱出。
- さらに</select>で親のselectを閉じた後、img onerrorなどの要素を差し込んでalert(1)を起動。
- ブラウザでアラートが出ればクリア。
実践:一手ずつ「なぜそうするか」を添えて
1) 「入力が使われている場所」を特定する
- 操作:任意の商品ページへ移動し、URL末尾に&storeId=abc123のように付加してページを再読込。
- 観察:ドロップダウン(在庫の店舗選択)にabc123が新しい選択肢として追加されている。
- なぜ:アプリがlocation.searchからstoreIdを取り出し、document.writeで<option>を生成している証拠です。典型的な脆弱コードは次のイメージです:
<script>
var storeId = new URLSearchParams(location.search).get('storeId');
// 危険:エスケープなしで生挿入
document.write('<option value="' + storeId + '">' + storeId + '</option>');
</script>
2) コンテキストを分析する(どこをどう壊す?)
- 状況:今いるのは<select>の中、かつ<option value="...">の属性値の最中です。
目標:以下の順で脱出します。
- " でvalue属性を閉じる
- > で<option>開始タグを閉じる
- </select> で<select>から脱出
- 任意の要素(例:<img src=x onerror=alert(1)>)を挿入して実行
3) ペイロードを設計する(最小ステップで動く形)
- 人間可読の形(説明用):
storeId="></select><img src=x onerror=alert(1)>
なぜ:
- 最初の"で value="... をクローズ
- 続く>で<option>のタグを確定
- </select>で親コンテナから完全脱出
- 続けてimg onerrorでスクリプト実行(エラー時にalert)
4) URLエンコードして実際に投下
- ブラウザに直接貼れるように、スペースなどをエンコードします:
/product?productId=1&storeId=%22%3E%3C%2Fselect%3E%3Cimg%20src%3Dx%20onerror%3Dalert(1)%3E
- なぜ:クエリ文字列中の"、<、>、スペース、=などはURLとしてエスケープしておくのが安全確実です。
5) 実行確認
- 操作:上記URLで商品ページを開く。
- 観察:ページ読込時にalert(1)が表示される。
- なぜ:document.writeはHTMLとして解釈・直挿入されるため、selectの外に飛び出した
要素のonerrorが即時実行されます。
ペイロード設計の考え方(分解して理解する)
- ソース: location.search(攻撃者完全制御の入力)
- シンク: document.write(HTMLとして解釈される最危険シンクの一つ)
- コンテキスト: select内、optionの属性値 → まず属性を閉じる発想が必要
- 脱出シーケンス: " → > → </select> → 悪性要素
- 実行トリガ: onerror(画像読み込み失敗で確実に発火)
つまずきポイント&対処
アラートが出ない
- URLのエンコード漏れがないか確認(特に " < > スペース =)。
- src=1 でも基本OKだが、確実性のため src=x など実在しない値に。
- ページがキャッシュしている場合はハードリロード。
ペイロードがドロップダウンの選択肢としてそのまま見えるだけ
- 脱出に必要な最初のダブルクォートが欠けている可能性。
storeId="から始めているか見直す。
- 脱出に必要な最初のダブルクォートが欠けている可能性。
CSPが邪魔している?
- このLABは通常CSPが緩い想定。もし厳しい環境なら、イベントハンドラ(onerror)やjavascript:URIの可否など、方針を切り替える。
実務目線の防御策
- document.writeの使用禁止(歴史的負債。動的UIはDOM APIで組み立てる)
const opt = document.createElement('option');
opt.value = storeId; // 値はそのまま代入(HTMLにならない)
opt.textContent = storeId; // テキストとして挿入(エスケープ不要)
select.appendChild(opt);
- エスケープ/サニタイズの徹底 HTMLとして注入しない。どうしてもinnerHTML等を使うなら、DOMPurifyなどの実績あるライブラリを用いる。
- 許可リスト(allowlist)による入力検証 storeIdは数値や既知IDのみを受け付ける。
- CSP(Content Security Policy) 'unsafe-inline'を避け、script-srcでインラインイベントハンドラを無効化。
- セキュリティレビューで「ソース→シンク」を辿る location、document、storage等の信頼できないソースが、innerHTML、document.writeなどの危険シンクへ渡っていないかを静的・動的に点検。
コピペ用ペイロード(そのまま使える)
- 説明と同じ内容(スペース等をURLエンコード済み):
/product?productId=1&storeId=%22%3E%3C%2Fselect%3E%3Cimg%20src%3Dx%20onerror%3Dalert(1)%3E
- ヒューマンリーダブル(ブラウザに貼る前に自分でエンコードしてね):
storeId="></select><img src=x onerror=alert(1)>
まとめ
ポイントは、「どのコンテキストでHTMLが生成されているか」を見極めること。今回は<select>配下の<option value="…">という属性値コンテキストから始まるため、まず属性を閉じてタグを抜ける、次に親要素を閉じる、最後に実行トリガを持つ要素を差し込む、という三段ロケットで成立します。 この「ソース(location.search)→ シンク(document.write)→ コンテキスト(select/optionの属性)→ 脱出シーケンス」という思考パターンは、他のDOM XSSでもそのまま応用できます。
Best regards, (^^ゞ