JavaScript | ES6+ 文法:テンプレート文字列 – XSS 対策の注意点

JavaScript
スポンサーリンク

テンプレート文字列と XSS の基本認識

テンプレート文字列はバッククォートで囲み、${...} に式を埋め込める便利な仕組みですが、ここが重要です:テンプレート文字列自体には自動エスケープ機能がありません。ユーザー入力や外部データをそのまま HTML へ差し込むと、スクリプトが実行される危険(XSS)が生じます。まず「どのコンテキスト(本文・属性・URL・イベントなど)に挿入するか」を意識し、適切なエスケープと DOM API を使い分けることが防御の土台です。

// 危険例:ユーザー入力をそのまま innerHTML に
const name = `<img src=x onerror=alert('XSS')>`;
document.querySelector("#out").innerHTML = `
  <p>ようこそ、${name} さん</p>
`; // ← onerror が走る可能性
JavaScript

安全な挿入先の選び方(DOM API を優先)

テキストは textContent を使う

HTML として解釈させず、純粋なテキストとして挿入するのが最も安全です。テンプレートで構造(タグ)だけ組み、内容(ユーザー入力)は textContent に渡す方針を基本にします。

const out = document.querySelector("#out");
const name = `<Alice>`; // 仮に悪意ある文字列でも…

const p = document.createElement("p");
p.textContent = `ようこそ、${name} さん`; // テキストとして安全に挿入
out.appendChild(p);
JavaScript

属性値は setAttribute / dataset を使う

属性へ直接テンプレートで埋め込むと、クォート閉じやイベント注入の危険が高まります。DOM API に任せればブラウザが適切に扱ってくれます。

const btn = document.createElement("button");
btn.className = "buy";
btn.setAttribute("title", `購入 "${name}"`); // 属性は API でセット
btn.dataset.id = String(7);                  // data-* は dataset が安全
JavaScript

URL は encodeURIComponent を徹底

クエリ文字列へ外部入力を入れる場合は必ずエンコードします。パスには encodeURI、クエリ値には encodeURIComponent を使い分けます。

const q = `JS 入門 & 実践`;
const url = `/search?q=${encodeURIComponent(q)}&page=${1}`;
JavaScript

HTML を文字列で組むときの前処理(サニタイズ)

本文用の最低限エスケープ

ここが重要です:< > & " ' の5文字を HTML エンティティへ置き換えるだけで、ほとんどの挿入型 XSS を回避できます。テンプレートに埋め込む前に必ず通します。

const escapeHtml = s => String(s).replace(/[&<>"']/g, c => ({
  "&": "&", "<": "<", ">": ">", '"': """, "'": "'"
}[c]));

const view = `
  <p>ようこそ、${escapeHtml(name)} さん</p>
`;
JavaScript

タグ付きテンプレートで一括防御

${...} に入る値を一律でエスケープし、連結を関数に任せれば“エスケープ漏れ”を防げます。

function html(strings, ...vals) {
  const safe = vals.map(escapeHtml);
  return strings.reduce((s, str, i) => s + str + (safe[i] ?? ""), "");
}

const safe = html`<p>ようこそ、${name} さん</p>`;
JavaScript

コンテキスト別の注意点(深掘り)

本文と属性では“必要なエスケープ”が異なる

本文は < > & " ' をエスケープ。属性は同様にエスケープしつつ、必ずクォートで囲み、ダブルクォート閉じに特に注意します。

const escAttr = s => String(s).replace(/[&<>"]/g, c => ({
  "&": "&", "<": "<", ">": ">", '"': """
}[c]));
const btnHtml = `<button title="${escAttr(name)}">購入</button>`;
JavaScript

危険なコンテキストに値を入れない

ここが重要です:テンプレートで以下のようなコンテキストを“直接”作らない。

  • JavaScript コンテキスト:onclick="..."href="javascript:..."
  • CSS コンテキスト:<style> .x { background: url("${user}"); } </style>
  • 危険な URL スキーム:javascript:, data: など。https/http のみ許可するホワイトリストで検証します。
// 悪い例:イベント属性へテンプレートで挿入
// <button onclick="${userInput}">…</button> ← 絶対に避ける
JavaScript

innerHTML は最後の手段

innerHTML は“HTMLとして解釈”されます。ユーザー入力を混ぜる必要があるなら、徹底したサニタイズ後に使うか、できれば DOM API を選びます。


実務で効く設計パターン(安全と保守性)

データは“先に安全化”、テンプレートは“見た目”だけ

テンプレート文字列は構造表現に集中させ、埋め込む値は事前に安全化したものだけを渡します。これが最もミスを減らします。

const sanitize = obj => ({
  id: String(obj.id),
  name: escapeHtml(obj.name),
  priceLabel: `${Number(obj.price).toLocaleString("ja-JP")} 円`
});

const renderCard = safe => `
<div class="card" data-id="${escAttr(safe.id)}">
  <h2>${safe.name}</h2>
  <p>価格: ${safe.priceLabel}</p>
</div>
`;

const safe = sanitize({ id: 1, name: `<Alice>`, price: 123456 });
document.body.insertAdjacentHTML("beforeend", renderCard(safe));
JavaScript

data-* で値を受け渡し、イベントは後で紐づける

イベント文字列をテンプレートに書かず、生成後に addEventListener を使います。安全でテストもしやすい方式です。

const buttonHtml = id => `<button class="buy" data-id="${escAttr(id)}">購入</button>`;
document.body.insertAdjacentHTML("beforeend", buttonHtml(7));
document.querySelectorAll(".buy").forEach(btn => {
  btn.addEventListener("click", e => {
    const id = e.currentTarget.dataset.id;
    // 安全な処理…
  });
});
JavaScript

エンコードの徹底(URL・CSV・JSON)

目的ごとに正しいエンコードを使います。間違ったエンコードは防御になりません。

// URL クエリ
const q = `JS 入門 & 実践`;
const searchUrl = `${base}?q=${encodeURIComponent(q)}`;

// CSV フィールド(ダブルクォート二重化)
const csvField = s => `"${String(s).replace(/"/g, '""')}"`;
JavaScript

例題で理解を固める

// 1) サニタイズ済み HTML をテンプレートで生成
const user = `<img src=x onerror=alert(1)>`;
const safeHtml = html`
  <section>
    <h1>ようこそ、${user} さん</h1>
    <p>安全なテンプレートです。</p>
  </section>
`;
document.querySelector("#out").innerHTML = safeHtml;

// 2) テーブル(本文・属性を適切に防御)
const rows = [
  { id: 1, name: `Apple`,    price: 100 },
  { id: 2, name: `<Banana>`, price: 200 },
];
const table = `
<table>
  <thead><tr><th>ID</th><th>商品名</th><th>価格</th></tr></thead>
  <tbody>
    ${rows.map(r => `
      <tr data-id="${escAttr(r.id)}">
        <td>${escAttr(r.id)}</td>
        <td>${escapeHtml(r.name)}</td>
        <td>${Number(r.price).toLocaleString("ja-JP")} 円</td>
      </tr>
    `).join("")}
  </tbody>
</table>
`;

// 3) 安全なリンク(URL エンコード+スキーム検証)
const toLink = (base, q) => {
  if (!/^https?:\/\//.test(base)) throw new Error("Invalid base URL"); // スキーム制限
  const safeQ = encodeURIComponent(q);
  const url = `${base}?q=${safeQ}`;
  return `<a href="${escAttr(url)}">検索</a>`;
};
JavaScript

まとめ

XSS 対策の核心は「テンプレート文字列は自動で守ってくれない。挿入するコンテキストごとに、正しい API とエンコードを使う」ことです。テキストは textContent、属性は setAttribute/dataset、URL は encodeURIComponent。HTML 文字列で組む必要があるなら escapeHtml やタグ付きテンプレートで一括防御し、イベント属性や javascript: スキーム、style 内の動的埋め込みは避ける。見た目はテンプレート、データは安全化してから埋め込む—この習慣が、安全で読みやすい ES6+ コードを支えます。

タイトルとURLをコピーしました