一覧SD-03-2 · XSSと出力の安全性

出力先に応じたエスケープ処理

値を構造へ混ぜず、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による直列化には&lt;や&amp;が現れます。文字列を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試料

1
window.host = document.getElementById("scratch"); window.text = "<em>sample & text</em>"; host.textContent = text; console.log(host.textContent,host.innerHTML);
<em>sample & text</em> &lt;em&gt;sample &amp; text&lt;/em&gt;
2
window.value = `" data-added="yes`; host.innerHTML = `<input value="${value}">`; console.log(host.firstElementChild.getAttribute("value"),host.firstElementChild.getAttribute("data-added"));
 yes
3
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
4
window.candidate = new URL("javascript:1+1"); console.log(candidate.protocol); console.log(candidate.protocol === "https:");
javascript:
false
5
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
6
window.parsed = new URL(link.href); console.log(parsed.searchParams.get("q"),parsed.searchParams.has("role"));
a&role=admin false
7
link.textContent = "<em>検索</em>"; console.log(link.querySelectorAll("em").length,link.textContent,new URL(link.href).hostname);
0 <em>検索</em> docs.example.test

演習完了