出力先に応じたエスケープ処理
値を構造へ混ぜず、URLのように追加の意味を持つ項目も検証します。
説明
HTML本文、引用符で囲んだ属性値、URLでは、文字が持つ意味が違います。一つのエスケープ関数をどこへでも適用するのではなく、出力する文脈に合うAPIと値の検証を選びます。
同じ表示領域で本文と属性の組立てを比較します。属性を増やしてしまう文字列、DOM APIでの値設定、URLのschemeと検索パラメーターの構築を順に確認します。
値を構造へ混ぜず、URLのように追加の意味を持つ項目も検証します。
1本文へHTMLに見える文字を出す
表示したい値をタグ付きの文字列にします。
window.host = document.getElementById("scratch");
window.text = "<em>sample & text</em>";
host.textContent = text本文として文字を設定する;
console.log(host.textContent,host.innerHTMLDOMをHTMLへ直列化した形を読む);textContentには元の文字列、innerHTMLによる直列化には<や&が現れます。文字列をHTMLソースへ直列化する際に特殊文字が区別されています。
2属性値へ引用符を連結する
入力中の引用符がvalue属性を閉じる例です。追加される属性は観察用のdata-addedです。
window.value = `" data-added="yes`;
host.innerHTML =属性を文字列連結で組み立てる `<input value="${value}">`;
console.log(host.firstElementChild.getAttribute("value"),host.firstElementChild.getAttribute("data-added")入力から増えた属性を確認する);valueは空になり、data-addedがyesとして追加されます。HTMLの引用符の境界が入力で変わっています。
3要素を作って属性値だけを設定する
DOM APIでinputを作り、値を別に設定します。
window.input = document.createElement("input")要素の種類を固定して作る;
input.setAttribute("value",value)属性名を固定して値だけを渡す;
host.replaceChildren(input);
console.log(input.getAttribute("value"),input.hasAttribute("data-added"));入力全体はvalueの値になり、data-added属性はありません。属性名はアプリ側の固定値です。イベント属性などへ同じ値を設定する話ではありません。
4URLにはschemeの検査も必要
hrefをDOM APIで設定しても、javascript:などの意味が自動で安全なURLへ変わるわけではありません。ここではURLを解析し、遷移は行いません。
window.candidate = new URL("javascript:1+1")URLを構造として解析する;
console.log(candidate.protocol);
console.log(candidate.protocol === "https:"許可するschemeと照合する);protocolはjavascript:で、HTTPSを受け付ける方針には合いません。HTMLのエスケープとURLの許可条件は別の処理です。
5許可するHTTPSのURLを構築する
リンク先はこの例で許可するdocs.example.testへ固定し、検索語だけを渡します。
window.link = document.createElement("a");
window.url = new URL("https://docs.example.test/search");
url.searchParams.set検索語をURLの値として設定する("q","a&role=admin");
link.href = url.href構築したURLをリンク先に設定する;
link.textContent = "検索";
host.replaceChildren(link);
console.log(link.href);&と=は検索語qの値として符号化されます。roleという別のパラメーターへ分かれていません。
6受け取るパラメーターを再解析する
できたURLから値を取り出し、別のパラメーターが増えていないか確認します。
window.parsed = new URL(link.href)生成したURLを再解析する;
console.log(parsed.searchParams.get("q"),parsed.searchParams.has("role")余分なパラメーターがないことを確認する);qはa&role=adminという一つの値で、roleは存在しません。符号化後も送受信の解釈が一致することを確かめます。
7表示文字と遷移先を別々に設定する
リンクの表示文字をHTMLのような文字列に変えます。遷移先URLはそのままです。
link.textContent =リンクの表示文字だけを変更する "<em>検索</em>";
console.log(link.querySelectorAll("em").length,link.textContent,new URL(link.href).hostname);em要素はなく、表示は文字列です。表示内容とリンク先という別の文脈を、それぞれ適切なAPIで扱っています。
まとめ
本文、属性値、URLで必要な処理を分けます。
属性名や要素名はアプリ側で固定し、値をDOM APIへ渡します。
URLはscheme・宛先の方針を確認し、パラメーターを構造として組み立てます。
操作履歴と解説を振り返れます。
JavaScript / ブラウザのDOM・CSP / 固定のHTML試料
window.host = document.getElementById("scratch");
window.text = "<em>sample & text</em>";
host.textContent = text;
console.log(host.textContent,host.innerHTML);<em>sample & text</em> <em>sample & text</em>
window.value = `" data-added="yes`;
host.innerHTML = `<input value="${value}">`;
console.log(host.firstElementChild.getAttribute("value"),host.firstElementChild.getAttribute("data-added"));yes
window.input = document.createElement("input");
input.setAttribute("value",value);
host.replaceChildren(input);
console.log(input.getAttribute("value"),input.hasAttribute("data-added"));" data-added="yes false
window.candidate = new URL("javascript:1+1");
console.log(candidate.protocol);
console.log(candidate.protocol === "https:");javascript: false
window.link = document.createElement("a");
window.url = new URL("https://docs.example.test/search");
url.searchParams.set("q","a&role=admin");
link.href = url.href;
link.textContent = "検索";
host.replaceChildren(link);
console.log(link.href);https://docs.example.test/search?q=a%26role%3Dadmin
window.parsed = new URL(link.href);
console.log(parsed.searchParams.get("q"),parsed.searchParams.has("role"));a&role=admin false
link.textContent = "<em>検索</em>";
console.log(link.querySelectorAll("em").length,link.textContent,new URL(link.href).hostname);0 <em>検索</em> docs.example.test
演習完了