script タグ中の JSON 埋め込みが XSS になる
- カテゴリ:
- JavaScript
- コメント数:
- Comments: 0
◆ JSON 化して script タグ中にデータを埋め込むとき
◆ JSON 化するデータの文字列中に 「</script>」 という文字列があると XSS できてしまう
◆ JSON 化するデータの文字列中に 「</script>」 という文字列があると XSS できてしまう
意外なところで XSS
サーバ側のデータをブラウザに渡すために script タグ中に JSON を埋め込むことがよくあります<script>
const SERVER_DATA = {{json}}
</script>
という感じのものです
Node.js でテンプレートリテラルを使うと
const render = values => `
<script>
const SERVER_DATA = ${JSON.stringify(values)}
</script>
`
という感じです
JSON 文字列化してるのでユーザ入力が入っても大丈夫 と思ってましたが大丈夫じゃありませんでした
ユーザ入力に 「</script>」 があるとそこで script タグが終了するので 新たに script タグを作ってそこで alert を書けば実行できてしまいます
例
例えばこういう ウェブサーバがあったとしますrequire("http").createServer((req, res) => {
const url = new URL(req.url, "http://localhost/")
const values = Object.fromEntries(url.searchParams.entries())
const body = view(values)
res.end(body)
}).listen(8000)
const view = (values) => `
<!DOCTYPE html>
<h1></h1>
<p></p>
<script>
const initPage = (data) => {
document.title = data.title
document.querySelector("h1").textContent = data.title
document.querySelector("p").textContent = data.text
}
const user_data = ${JSON.stringify(values)}
initPage(user_data)
</script>
`
クエリパラメータをオブジェクト形式にして JSON で埋め込んでいます
ページ内の JavaScript の処理として 埋め込んだ値を使って title や h1 を作っています
これだけだと直接 title や h1 タグ中に埋め込めば良いですが あくまでシンプルな例にするためなのでそこは気にしないことにします
このサーバに localhost でアクセスできるとして
http://localhost:8000/?title=abc&text=xyz
こういうクエリパラメータでアクセスするのが正常な使い方です
クエリパラメータ内に
</script><script>alert(1)</script>
を埋め込んでみます
http://localhost:8000/?title=abc&text=%3C/script%3E%3Cscript%3Ealert(1)%3C/script%3E
この URL にアクセスすると alert(1) が実行されてしまいます
対処
XSS といえば HTML エスケープ なのですがここでは HTML エスケープは使えませんscript タグの中で & や < が入るとそれはそのまま JavaScript として扱われます
JavaScript として構文エラーです
他とは違う特殊な対応が必要です
方法 1: JSON 用に別の script タグを作る
いくつかやり方があるのですが 普段何気なく使っていた独立した script タグが扱いやすい気がします<script type="application/json" id="server-data">
{{escaped_json}}
</script>
<script>
const SERVER_DATA = unescapeHTML(document.getElementById("server-data").textContent)
</script>
script タグ中にエスケープした JSON があると構文エラーになってしまうので別の script タグに分けます
type を application/json としておくことで JavaScript として実行はしないようにします
script タグでは textContent で取得してもエスケープは解除されないので 自分でエスケープ解除が必要です
上のコード中の unescapeHTML という関数は標準ではないので自分で用意します
JavaScript では HTML のエスケープや解除が少し面倒なので サーバが Node.js なら encodeURIComponent / decodeURIComponent を使うこともできます
「</script>」 という文字列が HTML 中になくなればいいので encodeURIComponent でも条件は満たせます
encodeURIComponent("</script>")
// "%3C%2Fscript%3E"
方法 2: / のエスケープ
「</script>」 が問題なので これを見つけた場合に置換してしまうのが 単純な考え方ですですが HTML は空白文字が許可されてるので正規表現マッチングになる上に少し面倒です
「</script>」 に限らずすべての 「/」 をエスケープしてしまう方が簡単です
JSON という前提があるので 「/」 が出現する可能性があるのは文字列の中のみです
JavaScript の構文に影響することはありません
const values = {"a/b": "c/d"}
console.log(JSON.stringify(values).replace(/\//g, "\\/"))
// {"a\/b":"c\/d"}
「\/」 じゃなくて 「\x2f」 にしても良いです
毎回置換するのは面倒なので html タグ関数を用意してエスケープするなら json にも対応しておくと楽に書けます
const escapeHTML = (str) => String(str).replace(/[<>&"]/g, c => {
if (c === "<") return "<"
if (c === ">") return ">"
if (c === "&") return "&"
if (c === '"') return """
})
const html = (tpls, ...values) => {
const tohtml = (value) => {
if (value == null || value === false) return ""
if (Array.isArray(value)) return value.map(tohtml).join("")
if (value.html) return value.html
if (value.json) return JSON.stringify(value.json).replace(/\//g, "\\/")
return escapeHTML(value)
}
return String.raw(tpls, ...values.map(tohtml))
}
html`
escape: ${value}
no escape: ${{ html: value }}
json: ${{ json: value }}
`
追記: テンプレートエンジンを使うと
Node.js だとテンプレートリテラルが使えるようになって テンプレートエンジンを使わなくなりましたが 本来のテンプレートエンジンならこういうところもちゃんとサポートされていて こういう問題は起きないよね と思って調べてみましたただシンプルなものが多いのでテンプレートエンジン側で JSON 出力をサポートしているものはほぼありませんでした
検索して上位に出てくる ejs, mustache, handlebars, hogan, doT ではサポートされていません
pug は少し特殊な上 普段使わないので確実とは言えませんが 軽く見た感じではそういう機能はなさそうでした
これらでは自分で JSON.stringify を使って文字列化することになるので 使う側が考慮してないと XSS が発生するかもしれません
PHP の Twig を Node.js に移植した Twing では json_encode フィルタがありました
PHP の json_encode はデフォルトで 「/」 をエスケープするので これは大丈夫そうと思ったのですが 中身は JSON.stringify でした
https://github.com/NightlyCommit/twing/blob/v5.0.2/src/lib/extension/core/filters/json-encode.ts
また Nunjucks にも dump フィルタで JSON 化する機能があったのですが JSON.stringify でした
https://github.com/mozilla/nunjucks/blob/v3.2.2/nunjucks/src/filters.js#L128
「</script>」 に対処するならフィルタを自分で作らないとダメのようです
https://stackoverflow.com/questions/46426306/how-to-safely-render-json-into-an-inline-script-using-nunjucks
テンプレートエンジンを使っていても 「</script>」 には対応してくれてなさそうです
テンプレートエンジンの issue を見ていると script タグの中に JSON を埋め込みたいという質問があって その回答が 「JSON.stringify をして HTML エスケープなしで埋め込めばできるよ」 みたいのもあるくらいなので この XSS は対処されてないところが意外と多そうな気もします