Trusted Types API とはなんぞや
結論
DOM XSS
攻撃を防ぐためのWeb標準API
です。
(利用できるブラウザは限られています。)
参考
サンプルアプリ
DOM XSS対策をしているフォーム、していないフォームの比較サンプル
CSPとTrusted Types APIを連携したサンプル
前置き
先日のVue Fesで以下のような発表がありました。
(Vue3.5からですが)こちらを利用することでv-html
を利用してもDOM XSS攻撃を防げるということです、素敵ですね。
ただ、そもそもTrusted Types API
というものの存在自体を僕は知りませんでした。なのでこれを機に調べてみつつ、サンプルアプリを作ってみました。
XSS攻撃とは?
簡単に言うとユーザーがクライアント側から悪意のあるコード・スクリプトをWebサイト内に埋め込む手法
です。サーバーサイド、クライアントサイド両方で脅威となる場合があります。
参考: MDN Docs: クロスサイトスクリプティング (XSS)
今回は、クライアントサイド側のXSS(DOM XSS)についてのお話です。ここからはこちらのサンプルアプリを利用しつつ説明したいと思います。よければ皆さんも一緒に触ってみてください。
説明
まずはDOM XSSがどういうものか実際に試してみましょう。
安全ではない実装のinputにサンプルコードを入力、そして表示ボタンをクリックします。(ここでは2.のコードを利用しました)
出力された文字の上にカーソルを移動すると、アラートが表示されまます。このように、ユーザーが任意のスクリプトを実行することができてしまいました。
ただのアラートであれば可愛いものですが、これが情報を漏洩させるようなコードだったら、、、?怖いですね。
もちろんですが、Zennではきちんと対策されています。
<div onmouseover='alert(2)'>マウスを上に置いてください</div>
現状ではエスケープ処理やサニタイズ処理を行いコードを無害化して出力する方法や CSPと呼ばれるHTTPヘッダーでスクリプトの実行を制限する設定を用いる方法があります。
以下は replaceを用いてコードをエスケープ処理したサンプルになります。
HTMLタグも含め、入力した値が丸ごと表示されています。文字の上にカーソルを置いてもアラートは表示されません。コンソールログを見るとエンティティに変換することで無害化されています。
window.displayReplaced = function () {
const input = document.getElementById('replaceInput').value;
const replaced = input.replace(
/[&<>""']/g,
(match) =>
({
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
}[match])
);
console.log('replaced: ', replaced);
document.getElementById('replaceOutput').innerHTML = replaced;
};
そして、新たなDOM XSS攻撃対策の手段として実装されたものがTrusted Types API
になります。
Trusted Types API とは
特定の型に基づいたオブジェクトに変換するポリシーを作成して、innerHTMLなどのDOMに安全なHTMLを挿入するためのAPIです。
以下はtrusted Types APIを用いた二つの例です。
上は入力された値をエスケープ処理するポリシーです。ログを見るとTrustedHTMLオブジェクトになっていることがわかります。
window.displayUseTrustedTypes = function () {
const input = document.getElementById('trustedInput').value;
const escapeHTMLPolicy = trustedTypes.createPolicy('myEscapePolicy', {
createHTML: (string) =>
string
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, '''),
});
const escaped = escapeHTMLPolicy.createHTML(input);
console.log('trustedtypes', escaped);
document.getElementById('trustedOutput').innerHTML = escaped;
};
下は入力した値ではなく、別の文字が出力されていますね。こちらはXSSの可能性がある文字パターンの場合、エラーを出力するポリシーにしています。エラーなのでログには何も表示されません。
window.displayUseTrustedTypesError = function () {
const input = document.getElementById('trustedErrorInput').value;
const escapeHTMLPolicy = trustedTypes.createPolicy('myErrorPolicy', {
createHTML: (string) => {
// XSSの可能性がある文字列パターンをチェック
const xssPatterns = [
/<script\b[^>]*>/i,
/javascript:/i,
/data:/i,
/on\w+\s*=/i,
/style\s*=/i,
];
// XSSパターンが検出された場合はエラーを投げる
for (const pattern of xssPatterns) {
if (pattern.test(string)) {
throw new Error('潜在的なXSSが検出されました: ' + string);
}
}
// 安全な文字列のみを許可
const div = document.createElement('div');
div.textContent = string;
return div.innerHTML;
},
});
try {
const escaped = escapeHTMLPolicy.createHTML(input);
console.log('trustedtypesError', escaped);
document.getElementById('trustedErrorOutput').innerHTML = escaped;
} catch (e) {
document.getElementById('trustedErrorOutput').textContent =
'⚠️ エラー: ' + e.message;
}
};
他にも、CSPと併用してさらに強固な実装を行うこともできます。
Web.dev: Trusted Types で DOM ベースのクロスサイト スクリプティングの脆弱性を防止
今回のような簡単なサンプルの場合では、replaceでも十分な対策はできます。しかしより巨大な構造になってくると、CSPと併用ができたり、TrustedHTML型など特別な型でのチェックができるTrusted Types APIが有用になってきます。
というわけで、せっかくなのでCSPをヘッダーに設定してTrusted Types APIと連携させた別のサンプルアプリを用意しました。
Trusted Types API & CSP
上がcreatePolicyを利用したInput、下が利用していないInputです。
画面を見ると、上は値が表示されていますが、下は意図しない文字が表示されています。コンソールログを見るとエラーが出力されていますね。
TypeError: Failed to set the 'innerHTML' property on 'Element': This document requires 'TrustedHTML' assignment.
at HTMLButtonElement.<anonymous> (main.js:68:27)
このようにCSPとTrusted Types API を利用することで、XSS DOM攻撃に対する対策漏れがない設計にすることができます。
利用上の注意
ただ、こちらまだ新しい技術ということで、残念ながら利用できるブラウザが限られてしまいます。
2024/10/25現在では以下のような対応表となっています。
こちらはSafariですが、エラーになります。
また、Trusted Types APIには他にもさまざまな機能やユースケース・制約があります。今回は書ききれなかったので、詳しくはMDNなどを見て調べてみてください。
終わりに
ということで、Vue.jsでもTrusted Types APIに対応することで、さらにセキュリティ対策の幅が広がるということです。いいことですね。v-htmlはXSSの原因になるから絶対に使ってはいけない。と口を酸っぱく言われるのがいずれ懐かしくなる日が来そうです。
Discussion