HTML インジェクションとは何か
HTML インジェクションは、外部から渡された文字列が「HTMLとして解釈・実行されてしまう」問題です。表示したい“文字”のはずがタグとして扱われると、画面の改ざん、リンクの差し替え、フォームの偽装などが起こります。ここが重要です:インジェクションは「表示だけの問題」ではなく、XSS(スクリプト注入)につながり、ユーザーの情報や操作を盗まれる重大なセキュリティ事故になります。
どう起きるのか(危険なパターンを理解する)
innerHTML に外部データをそのまま入れる
<p id="msg"></p>
<script>
const msg = document.getElementById("msg");
const userInput = `<img src=x onerror=alert('攻撃')>`; // ユーザー入力の例
msg.innerHTML = userInput; // HTMLとして展開される(危険)
</script>
HTMLこのコードは、入力の中のタグや属性が“解釈”されます。結果、意図しない動作(イベント発火や画面改ざん)が起きます。ここが重要です:外部データを innerHTML に直接渡すのは禁物です。
文字列テンプレートに直接差し込む
const name = "<b>太郎</b>";
const html = `<p>ようこそ ${name} さん</p>`; // そのまま混ぜるのは危険
container.innerHTML = html;
JavaScript一見ただの装飾でも、「誰が提供した文字列か」を見失うと、攻撃コードを混ぜる余地が生まれます。
何が起こりうるか(被害の具体像)
画面の偽装・入力の誘導
攻撃者がボタン風の偽要素や偽フォームを注入し、本物とすり替えれば、ユーザーは騙されてクリックや入力をしてしまいます。ここが重要です:画面が正しく見えていても、DOM が書き換わっていると操作の意味が変わってしまいます。
XSS による情報窃取・セッション乗っ取り
スクリプトが注入されると、Cookie・入力中の内容・クリックイベントなど“ユーザー操作”が盗まれる可能性があります。ここが重要です:XSS はユーザーに直接被害が及ぶため、最優先で防ぐべきリスクです。
安全な対策の軸(文字として扱う・構造は固定する)
textContent を使う(最重要の基本)
const msg = document.getElementById("msg");
const userInput = `<img src=x onerror=alert('攻撃')>`;
msg.textContent = userInput; // タグは「文字」として表示(安全)
JavaScripttextContent はタグを解釈しないため、外部データやユーザー入力は“常に文字として”差し込むのが大原則です。ここが重要です:迷ったら textContent、一択です。
構造はテンプレートで用意、可変部分だけ文字で差し込む
<p>ようこそ、<span id="name"></span> さん</p>
<script>
const nameEl = document.getElementById("name");
nameEl.textContent = userInput; // 可変部分のみ文字で
</script>
HTMLHTML の骨組みは固定し、変わるのは“テキストだけ”。これが保守と安全性の両立につながります。
innerHTML が必要なときの限定的な使い方(厳格管理)
エスケープしてから差し込む
function escapeHTML(s) {
return String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''})[c]);
}
const safeName = escapeHTML(userInput);
container.innerHTML = `<p>ようこそ <strong>${safeName}</strong> さん</p>`;
JavaScript動的に混ぜるのは“文字だけ”。構造(strong や p)は固定テンプレートで、外部データは必ずエスケープします。ここが重要です:テンプレートの構造を最小に保ち、動的部分は全面エスケープ。
テンプレート要素を使った安全なレンダリング
<template id="row">
<li><span class="name"></span> <span class="qty"></span></li>
</template>
<ul id="list"></ul>
<script>
const tpl = document.getElementById("row");
const list = document.getElementById("list");
const frag = document.createDocumentFragment();
data.forEach(item => {
const node = tpl.content.cloneNode(true);
node.querySelector(".name").textContent = item.name;
node.querySelector(".qty").textContent = String(item.qty);
frag.append(node);
});
list.append(frag);
</script>
HTMLテンプレートは“実行されない”ので安全に構造を用意でき、可変部分は textContent で埋められます。
実装時に必ず意識するチェックポイント(深掘り)
データの出どころを常に分類する
- ユーザー入力、URL パラメータ、外部 API の値は「不信頼」。必ず textContent(またはエスケープ)で扱う。
- 自前で定義した固定文字列は「信頼」。それでも innerHTML の使いすぎは避ける。
ここが重要です:入力経路を“信頼/不信頼”で頭の中に色分けする癖を付けると、事故が激減します。
置き換えでイベントが消えることを理解する
innerHTML による全置換は、既存の子要素ごと破棄します。イベントハンドラも消えるため、再登録の漏れがバグの原因になります。部分更新は DOM API(createElement、append、classList、textContent)に寄せましょう。
画面の“見え方”と実際の DOM は違う
CSS で非表示にした要素も、innerHTML ではそのまま置き換え可能です。目視では“変わっていない”ように見えても、DOM 側は改ざんできてしまいます。ここが重要です:見た目に騙されず、コードで安全策を徹底する。
補強策(技術的ガードで守りを固める)
コンテンツセキュリティポリシー(CSP)
インラインスクリプトや外部ドメインからの読み込みを制限するブラウザ側の防壁です。万一の注入時も被害範囲を狭められます。ここが重要です:アプリの設定で“実行できるソース”を絞ると、攻撃成功率が下がります。
HTML サニタイズ(ライブラリ利用)
どうしてもリッチな HTML を受け入れる必要がある場合、許可タグと属性のみを残す“サニタイズ”を導入します。ここが重要です:自前で全ケースを網羅するのは困難、実績あるサニタイザを使う方が安全です。
出力の一元化
「画面に文字を出す関数」を1カ所に集約し、その中で textContent・エスケープ・テンプレート適用を統一します。ここが重要です:出力経路が散らばると漏れます。共通関数で“必ず安全に出す”仕組みに。
例:安全なメッセージ表示の設計
<p id="notice"></p>
<script>
const notice = document.getElementById("notice");
function showMessage(type, text) {
notice.className = ""; // クラスをリセット
notice.classList.add(`is-${type}`); // 見た目はクラスで制御
notice.textContent = String(text ?? ""); // 文言は文字として安全に
}
showMessage("ok", "準備が完了しました");
showMessage("error", "<b>失敗</b>"); // タグはそのまま文字表示(安全)
</script>
HTML構造・装飾は CSS クラス、可変部分は textContent。これが最も安全で、読みやすい基本形です。
まとめ
HTML インジェクションは、外部データが“HTMLとして解釈される”ことで起き、画面の偽装や XSS に直結します。防ぐ鍵は、可変データを常に“文字として”扱うこと(textContent)、構造は固定テンプレートで用意し、innerHTML は厳格に管理された限定的な場面に限ること。入力の出どころを常に意識し、置き換えによるイベント消失や再描画コストも理解する。CSP・サニタイズ・出力の一元化で守りを固める。これらを徹底すれば、初心者でも安全で堅牢な UI を作れます。
