0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

#0205(2025/08/11)XSSの流れと応用的な対策

Posted at

XSSの流れと応用的な対策:HTML/CSS埋め込みの実務ルール

要点:HTML/CSSを“文字列連結で作らない”。許可リスト+文脈別エスケープ+安全API+CSPの多層防御で、埋め込み需要と安全性を両立する。


1. 何が難しいのか(応用編の前提)

  • 埋め込み対象が HTML本文 / 属性 / URL / CSS / JS文字列 / SVG と複数の文脈に跨り、各文脈で必要な防御が異なる
  • リッチテキスト・Markdown・WYSIWYGの普及により、「一部のタグだけ通したい」 ニーズが増加。
  • ブラウザは壊れたマークアップも寛容に補正するため、少しの抜けが実行に直結。
[未信頼入力] → [型/長さ/スキーム検証] → [サニタイズ(必要最小限)]
                 ↓                                  ↓
              [出力文脈判定] → [文脈別エスケープ] → [安全APIでDOM構築]
                                      ↓
                                 [CSP/HttpOnlyで最終防御]

2. HTMLタグを「一部許可」する設計方針

2.1 許可リスト(allowlist)を基本に

  • 許可タグは最小限p, br, strong, em, ul, ol, li, code, pre, a, img など。
  • 禁止タグ(代表)script, style, iframe, object, embed, link, base, form, meta, svg
  • 禁止属性on*(全イベント), style, srcset, formaction, integrity, xlink:href など。
  • URL属性href, src 等)は スキーム許可リストhttps:// のみ等)。javascript: / data: は拒否。

2.2 サニタイズとエスケープの違い(比較)

目的 サニタイズ エスケープ
何をする 危険な要素/属性を削除・置換 危険文字をエンコード<&lt;
いつ使う 「一部HTMLを通す」時 すべての出力(文脈ごと)
失敗時の挙動 表示崩れ/情報欠落の可能性 ただのテキスト表示になる
注意点 許可範囲の設計が難しい 文脈(HTML/属性/JS/CSS)ごとにルールが違う

原則:まずエスケープ、やむを得ない箇所だけサニタイズ+許可リスト


3. 安全なDOM組み立てパターン

3.1 文字列でDOMを作らない

NG

div.innerHTML = userHtml; // 直描画はXSSの温床

OK

const p = document.createElement('p');
p.textContent = userText; // テキストとして表示
container.appendChild(p);

3.2 属性はAPIで設定

const a = document.createElement('a');
const url = userHref; // 未信頼
if (/^https:\/\//.test(url)) {
  a.setAttribute('href', url);
}
a.textContent = 'リンク';
// target利用時はタブなりすまし対策
a.rel = 'noopener noreferrer';
a.target = '_blank';

3.3 画像はプレースホルダと組み合わせる

const img = document.createElement('img');
const src = userSrc;
img.src = /^https:\/\//.test(src) ? src : '/img/placeholder.png';
img.alt = '';

SVGは原則禁止<script>や外部参照が混ざりやすい)。必要な場合はサーバ側で画像化(PNG/SVGサニタイズ済みなど)。


4. CSSを扱うときの注意点

4.1 ユーザー入力でCSSを生成しない

  • クラス割当で表現(列挙型にマップ)。
  • インラインstyleは数値/列挙のみ許可(単位・範囲検証)。
  • url() に未信頼入力を入れない。@import や外部参照を禁止。

NG

el.className = 'badge badge--' + userColor;      // 直連結
el.style.cssText = 'background:' + userInput;    // 値検証なし

OK

const MAP = { primary:'badge--primary', success:'badge--success', warning:'badge--warning' };
el.classList.add('badge', MAP[userColor] ?? 'badge--primary');

const size = Number(userSize);
if (Number.isFinite(size) && size >= 8 && size <= 48) {
  el.style.fontSize = `${size}px`;
}

4.2 セレクタを動的生成する場合

  • CSS.escape()セレクタ名にのみ使用(値のエスケープではない)。
  • カスタムプロパティ(--var)に未信頼文字列を入れない。

4.3 CSPでstyleも締める

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-<RANDOM>';
  style-src  'self' 'nonce-<RANDOM>';
  object-src 'none'; base-uri 'self'; frame-ancestors 'self';

style-src 'unsafe-inline' は極力避ける。どうしても必要なら nonce 方式 へ移行。


5. よくあるアンチパターンと対策(HTML/CSS)

アンチパターン 何が起きる 対策
innerHTML 直描画 任意タグ/イベント挿入 テキスト表示 or 許可リスト+サニタイズ+安全API
属性の非クォート 属性分断→onerror注入 すべて"…"で囲み、値はエスケープ
href="javascript:..." 即時実行 スキーム許可(https:// 等)で検証
srcset の野放し 外部トラッキング/実行経路 srcのみに限定 or ホスト固定
style許可 CSS経由の抜け道/外部参照 原則禁止。必要時は数値/列挙のみ
動的クラス直連結 クラス名汚染 サーバ定義の列挙にマップ
SVG許可 スクリプト混入 禁止 or 画像化/厳格サニタイズ
target=_blankのみ window.opener 悪用 rel="noopener noreferrer" 併用

6. ユースケース別レシピ

6.1 Markdownビューア(コード含む)

  • 方針:Markdown→HTML 変換後、許可タグ/属性でサニタイズ。リンクは https:// のみ、画像は プロキシ配信(同一オリジン化)。
  • コードブロックはテキストとして出力(ハイライトはサーバ/ワーカー側で安全に)。

疑似コード

const html = renderMarkdown(md);
const safe = sanitize(html, {
  tags: ['p','br','em','strong','ul','ol','li','code','pre','a'],
  attrs: { a:['href','rel','target'] },
  url: (u) => /^https:\/\//.test(u)
});
root.innerHTML = safe; // ※サニタイズ済みのみ

6.2 WYSIWYG(リッチテキスト)

  • 入力段階で貼り付けサニタイズ。保存前にもサーバで再サニタイズ(ダブルチェック)。
  • 画像/ファイルは自社ストレージへアップロード→発行URLに差し替え。

6.3 メールテンプレート編集(HTML許可)

  • 許可タグは極小。style/script/form系は不可。
  • 変数埋め込みは テンプレートエンジンの自動エスケープ を使用。raw/safe フラグは禁止。

7. サーバサイドの責務(最終ライン)

  • 文字コード固定Content-Type: text/html; charset=UTF-8
  • Cookie保護HttpOnly; Secure; SameSite
  • レスポンスヘッダ:CSP(script-src/style-srcにnonce)、X-Content-Type-Options: nosniff
  • 画像/SVGの取り扱い:外部URL直参照を避け、画像プロキシサーバ変換(SVG→PNG)

8. 自動テストに組み込む(最小セット)

  • ペイロード例:
"><img src=x onerror=alert(1)>
" onmouseover="alert(1)
javascript:alert(1)
<data:text/html,><svg/onload=alert(1)>
</script><script>alert(1)</script>
  • E2Eで「表示→無害なテキストとして出ること」「リンクがhttpsのみ」「target=_blankにrel付与」を検証。
  • CSP有効時、インラインスクリプトがブロックされることを確認(nonceなしで失敗するのが正)。

9. まとめ(応用編チェックリスト)

  • まずテキスト表示。HTML/CSSを通す箇所は最小限に限定
  • 許可タグ/属性の明文化とユニットテスト
  • URLはスキーム許可javascript:/data:を拒否
  • 属性値はエスケープ+必ずクォートで囲む
  • DOM構築は createElement/textContent/setAttribute
  • CSSはクラス列挙にマップ。インラインは数値/列挙のみ
  • SVGは原則禁止(必要時はサーバ変換)
  • target=_blank には rel="noopener noreferrer"
  • CSPで script-src/style-srcnonce 運用
  • 代表ペイロードでE2E自動化

付録図:HTML/CSS埋め込みの意思決定フロー

          [未信頼データを表示したい]
                       │
         ┌── テキスト表示で良い? ────────┐
         │  はい                       │ いいえ
         ▼                           ▼
   textContentで出力        [HTMLを通す必要がある]
                                      │
                     ┌── 許可タグ/属性は定義済み? ───────┐
                     │               はい             │ いいえ
                     ▼                                ▼
         サニタイズ→安全API組立             設計→許可リスト作成→テスト
                                                     │
                                                     ▼
                                          CSP/HttpOnlyで最終防御

この応用編を既存の「基本編」の基準に統合し、レビュー/テストの共通チェックリストとして運用すれば、HTML/CSS埋め込みを伴う機能でも堅牢性を維持できます。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?