jQueryのDOMベースXSS

jQueryによるDOMベースのクロスサイトスクリプティング

説明

textContent、innerText、value、attr などでDOMから取得したテキストや属性値を、後からHTMLとして解釈するとDOM XSSが起こる可能性があります。jQueryの $() は渡した文字列をマークアップとして解析する場合があり、.html() や .append() でもタグやイベントハンドラーを挿入できます。以前エスケープした内容をテキストとして読み出し、再びHTMLとして解釈すると、最初の出力時の保護が失われる場合があります。

想定される影響

  • スクリプトから読めるCookie、トークン、ブラウザーストレージの値を盗まれ、ユーザーになりすまされる可能性があります。
  • ページ内容、フォーム入力、CSRFトークンなどを読み取られ、外部へ送信されるおそれがあります。
  • DOMを改変され、フィッシング用のフォームやポップアップを表示される可能性があります。
  • ユーザーの権限でバックエンドAPIやアカウント設定を操作されるおそれがあります。
  • 属性、ストレージ、URLに残った値が、再訪問時にも解釈される可能性があります。

対処方法

  • DOMのテキストや属性値をHTMLとして解釈せず、textContent、innerText、jQueryの .text() で表示してください。
  • セレクターには jQuery($str) ではなく $.find(str) を使い、HTMLの解析を避けてください。許可リストで選択範囲も制限してください。
  • マークアップが必要なら、DOMPurifyなどの保守されているツールでサニタイズし、必要なタグと属性だけを許可してください。
  • 信頼できない文字列を .html()、.append()、.before()、.after() へ渡さず、用途に応じてテキストノードや安全に作成したDOMノードを使ってください。
  • query/hash、data-* 属性、フォーム値は、実際の用途に合わせて検証・サニタイズしてください。
  • 追加の防御として、CSPでインラインスクリプトを制限してください。

例

A・B・Cは、テキスト表示、制限したHTML表示、許可した要素の選択という別々の目的を示します。HTMLとして解析しないだけでは選択範囲は制限されないため、許可リストも維持してください。

変更前

javascript
// DOMから読んだ文字列をHTMLとして再解釈して挿入(危険)
$(function () {
  // テンプレート・Storage・URLから復元された、ユーザーが制御可能なdata-content
  const raw = $("#card").attr("data-content");
  // 1) 文字列をそのまま.html()に渡すとイベントハンドラーやスクリプト実行のおそれ
  $("#detail").html(raw);

  // 2) jQuery($str)がユーザーの文字列をHTMLとして解析する可能性
  const $frag = $(raw);
  $("#detail").append($frag);
});

変更後

javascript
// 文字列はテキスト、セレクターはセレクターとしてだけ解釈
// 任意のサニタイザー使用例も含む
$(function () {
  const raw = $("#card").attr("data-content");

  // (A) テキストとして表示
  $("#detail").text(raw); // HTMLとして再解釈しない

  // (B) HTMLが必要な場合はサニタイザーを使用
  // const safe = DOMPurify.sanitize(raw, {ALLOWED_TAGS: ['b','i'], ALLOWED_ATTR: []});
  // $('#detail').html(safe);

  // (C) ユーザー指定セレクターには$()でなく$.findを使用
  const sel = $("#btn").attr("data-target");
  // 許可リストやパターン検証の例
  const allowed = ["#info", ".section"];
  if (allowed.includes(sel)) {
    $.find(sel).forEach(function (el) {
      el.classList.add("active");
    });
  }
});

解説:

  • 変更前: ユーザーが制御できる属性値を .html() や $() に渡すと、マークアップとして解釈されます。イベントハンドラーを含む値でスクリプトを実行されるおそれがあります。
  • 変更後: .text() はHTMLとして解釈せずに表示します。必要なマークアップはDOMPurifyでサニタイズし、セレクターは $.find と許可リストで扱います。

参考資料