Vue vs. Content Security Policy
厳し目のCSPを適用したい
投稿系のアプリを作る際に、なるべくXSSの脅威を減らすため CSP を厳し目に設定したいケースがあります。
CSPでunsafe-evalを設定しない場合、Vueの 完全パッケージ ではエラーが出てしまい動かすことができません。
VueのCSPエラー
[Vue warn]: It seems you are using the standalone build of Vue.js in an environment
with Content Security Policy that prohibits unsafe-eval.
The template compiler cannot work in this environment.
Consider relaxing the policy to allow unsafe-eval or pre-compiling your templates into render functions.
## ざっくり日本語訳
[Vue warn]: スタンドアロンビルドのVue.jsを、evalが禁止された環境上で利用しています。
この環境ではテンプレートコンパイラは動きません。
evalの利用を許可するか、テンプレートをrender関数でプリコンパイルすることを検討してください。
今回はCSPのscript-srcで一番厳しいscript-src 'self';を目標に、どうやってVueを動かすかの解説を行います。 なおビルド環境はwebpackを利用しますが、基本的な考えは他のビルドツールやCDN配布のVueスクリプト利用でも変わりません。
CSP vs. Vue
下記はテスト用にCSPを設定したページとVueが書かれたjsファイルです。
CSPを設定したHTML
<!DOCTYPE html>
<html lang="ja">
<head>
<title>CSPテスト</title>
<meta http-equiv="Content-Security-Policy" content="script-src 'self';">
</head>
<body>
<main>
<span v-text="message"></span>
</main>
<script src="/build/test.js"></script>
</body>
</html>
test.js
import Vue from "vue";
new Vue({
el: "main",
data: {
message: "CSPテスト"
}
});
このままでは先程のevalエラーが出てしまうので、 テンプレートコンパイラの入っていないランタイムパッケージを利用します。
webpack.config.js
...
resolve: {
...
alias: {
...
// ランタイムパッケージを利用
"vue$": "vue/dist/vue.runtime.esm.js"
}
},
Symfony4のWebpackEncoreBundleを利用している場合は、下記のように設定します。
Webpack Encore版のwebpack.config.js
const config = Encore.getWebpackConfig();
config.resolve.alias["vue$"] = "vue/dist/vue.runtime.esm.js";
ランタイムパッケージに切り替えるとevalエラーは消えますが、今度は別のエラーが出ます。
ブラウザのデベロッパーコンソールに表示されるエラー
vue.runtime.esm.js:619 [Vue warn]: You are using the runtime-only build of Vue where the template compiler is not available.
Either pre-compile the templates into render functions, or use the compiler-included build.
## ざっくり日本語訳
vue.runtime.esm.js:619 [Vue warn]: テンプレートコンパイラの入っていないランタイムビルドのVueを利用しています。
テンプレートをrender関数でプリコンパイルするか、コンパイラの入ったパッケージを利用してください。
もちろんテンプレートコンパイラは使えないので、render関数でプリコンパイルする方法で対応します。
render関数を使う
render関数を使うためVueをコンポーネント化します。 Vueコンポーネントはいくつかありますが、今回はシングルファイルコンポーネント(SFC)で記述します。
Test.vue
<template>
<main>
<span>{{ message }}</span>
</main>
</template>
<script>
export default {
props: {
message: String,
},
}
</script>
SFCを書いたら、Vueのエントリーjsもrender関数に書き直します。
test.js
import Vue from "vue";
// 作成したSFCをインポートする
import Test from "./Test";
new Vue({
el: "main",
render(createElement) {
// SFCをレンダリングする
return createElement(Test, {
props: {
message: "CSPテスト",
}
});
},
});
最後にHTMLからv-textを外して完成です。
HTMLからバインド構文を除去
<!DOCTYPE html>
<html lang="ja">
<head>
<title>CSPテスト</title>
<meta http-equiv="Content-Security-Policy" content="script-src 'self';">
</head>
<body>
<main></main>
<script src="/build/test.js"></script>
</body>
</html>
これでCSPに違反せず、Vueが動くことを確認できると思います。
まとめ
今回はCSPのスクリプト設定で一番厳しいscript-src 'self';上でVueを動かしました。 ちなみにこの設定はインラインスクリプトも許可しないので、XSSの危険度をかなり下げることができます。 安全性が増す反面、外部サイトスクリプト(広告タグや計測タグ)を利用する場合は設定が面倒になるので、 リスク判断に応じて制限を緩めるのがオススメです。