テンプレート文字列と 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 が安全
JavaScriptURL は encodeURIComponent を徹底
クエリ文字列へ外部入力を入れる場合は必ずエンコードします。パスには encodeURI、クエリ値には encodeURIComponent を使い分けます。
const q = `JS 入門 & 実践`;
const url = `/search?q=${encodeURIComponent(q)}&page=${1}`;
JavaScriptHTML を文字列で組むときの前処理(サニタイズ)
本文用の最低限エスケープ
ここが重要です:< > & " ' の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> ← 絶対に避ける
JavaScriptinnerHTML は最後の手段
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));
JavaScriptdata-* で値を受け渡し、イベントは後で紐づける
イベント文字列をテンプレートに書かず、生成後に 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+ コードを支えます。
