なぜ危ないのか
AIはプロンプトに「インタラクティブなフォームを作って」と書くと、入力値を直接`innerHTML`に渡すコードを生成しがちだ。これはAIがXSSを意図しているわけではなく、手っ取り早く動くデモを返そうとした結果だが、受け取った側が内容を確認せず公開すると脆弱なHTMLが本番環境に載る。
生成されたHTMLをそのままzipにしてファイル便で送るケースも増えているが、受信者のPCでzipを展開してブラウザで開いた場合もXSSは発火する。ファイルプロトコルでのスクリプト実行はブラウザによって制限が異なり、ChromeとSafariで挙動が違うことも確認を複雑にしている。
フレームワークを使わない素のHTMLをAIで生成した場合は、ReactやVueが提供するエスケープ処理が自動では入らない。テンプレートリテラルで動的にHTMLを組み立てるパターンを含むコードが出力されることがあり、これを見落とすと後から修正コストが大きくなる。
- 相手がログインなしで開ける状態か確認する
- PCとスマホで最低1回ずつ表示を確認する
- 内部情報・個人情報・不要な外部送信が残っていないか見る
- レビュー期限と修正時の差し替え方を決めておく
ソースで見る場所
生成されたHTMLをスキャンするときはまず`innerHTML`の使用箇所を特定する。`grep -n 'innerHTML' output.html`を実行し、ヒットした行の右辺が固定文字列ではなく変数や関数の戻り値であれば要注意だ。変数の値がユーザー入力から来ている場合はXSSが成立する経路になる。
`eval`と`Function()`コンストラクタも同様にgrepで探す。AIがJSONのパースや動的な関数生成にこれらを使うことがあり、引数に外部から取得した文字列が渡されるとコードインジェクションになる。`new Function(`という文字列も忘れずに検索対象に含める。
スクリプトタグの`src`属性に`data:`URIや`javascript:`スキームが使われていないかも確認する。これらは一見無害に見えるが、Base64エンコードされたスクリプトを埋め込む際に悪用されるパターンがある。ブラウザの開発者ツールのNetworkタブでロードされるリソースを一覧表示してチェックするのが確実だ。
- HTML内の外部script・form action・iframeを確認する
- APIキーやトークンらしき文字列がないか検索する
- 画像・CSS・JSのパスが公開後も解決できるか見る
- noindexと認証を混同せず、必要なら両方設定する
安全に共有する設定
プロンプトレベルでXSSを予防するには、AIへの指示の末尾に「HTMLに埋め込むすべての動的な値はDOMPurifyまたはtextContentで出力し、innerHTMLへの直接代入は禁止」と明記するのが効果的だ。この制約を加えると、AIがgetElementById().textContent = userInputのような安全なパターンを選ぶ頻度が上がる。
生成後の自動スキャンをCIに組み込む場合は、Semgrepのルール`javascript.browser.security.insecure-document-method.insecure-document-method`が役立つ。HTMLファイルをリポジトリに置いているなら、GitHub Actionsのworkflowに`semgrep --config=p/xss`ステップを追加すれば、プッシュのたびにXSSパターンが検出される。
スキャンをパスしたHTMLを共有する際は、ギガサイト便のような会社ドメイン認証付きの共有サービスを使うと、意図しない相手のアクセスを防ぎながらレビューを完結できる。スキャン結果のログとURLの発行日時を社内ドキュメントに残しておくと、後から監査が必要になった際の証跡になる。
- HTML内の外部script・form action・iframeを確認する
- APIキーやトークンらしき文字列がないか検索する
- 画像・CSS・JSのパスが公開後も解決できるか見る
- noindexと認証を混同せず、必要なら両方設定する
再発防止ルール
プロジェクトのREADMEまたはAIプロンプトテンプレートに、XSS対策の制約文を定型句として追加しておく。「innerHTML禁止・eval禁止・外部スクリプトはSRI付きで読み込む」の三点セットをコピペできる形で用意しておけば、新しいメンバーが参加しても同じ品質で生成物を扱える。
週次または月次でリポジトリ内のHTMLをgrepスキャンするcronジョブを設定し、新たにコミットされたファイルにXSSパターンが混入していないかを継続監視する。Slackに通知するようにしておくと、検出した時点で素早く対処できる。
インシデントが発生した場合は、どのプロンプトがそのHTMLを生成したかを記録し、再発防止のためにプロンプトテンプレートを更新する。AIの出力はモデルのバージョンによって変わるため、定期的にサンプル生成して新バージョンが同じ安全なパターンを出力するか検証するサイクルを設けることが長期的な対策になる。
- HTML内の外部script・form action・iframeを確認する
- APIキーやトークンらしき文字列がないか検索する
- 画像・CSS・JSのパスが公開後も解決できるか見る
- noindexと認証を混同せず、必要なら両方設定する
よくある質問
DOMPurifyをHTML内に読み込むとき、そのCDN自体が乗っ取られるリスクはどう対処しますか?
SRI(Subresource Integrity)のintegrity属性をscriptタグに付与することで対処できる。DOMPurifyの公式GitHubリリースページにSRIハッシュが掲載されているので、それをコピーして使う。
Semgrepを使ったことがないのですが、ローカルで手軽に試すにはどうすればよいですか?
`pip install semgrep`またはHomebrewで入れた後、HTMLファイルのあるディレクトリで`semgrep --config=p/xss .`を実行する。初回はルールのダウンロードに数十秒かかるが、結果はターミナルに即表示される。
textContentを使えばXSSは完全に防げますか?
textContentはHTMLタグをエスケープしてテキストとして挿入するため、innerHTMLを使った直接代入より安全だ。ただしscript属性経由のXSSは別経路で発生するため、イベントハンドラ属性の排除も合わせて行う必要がある。