ブラウザには、Same-Origin Policy(同一生成元ポリシー)やcookieのsamesite属性をはじめとした、セキュリティに関する仕組みが多々あります。今回はクロスサイトスクリプティング(以降XSS)のリスク軽減策としても使える「コンテンツセキュリティポリシー(以降CSP)」について、紹介したいと思います。
クロスサイトスクリプティング(XSS)とはどういう攻撃か
ご存知の方も多いと思いますが、まず最初にXSSとはどういう攻撃なのか簡単に説明します。
XSSとは、ユーザーが入力した文字列を出力する際に、エスケープやサニタイズを行わないまま出力することが原因で、入力した文字列がJavaScriptとして実行できてしまう脆弱性のことを指します。
サイト上(ドメイン・オリジン上)で任意のJavaScriptを実行できるため、下記のような被害等が発生する可能性があります。
- セッションID等を持たせているHttpOnly属性のないCookieの取得
- Web Strage内で持っているアクセストークン等の取得
- 本来は存在しない情報の表示
また近年ではXSSを起点として派生した攻撃からコンテンツの改ざん等が行われた結果、ECサイト等でクレジットカード情報の漏洩に繋がってしまうような事例も目にします。
XSSの対策としては、ユーザーが入力した値を出力する際にエスケープやサニタイズを徹底することが挙げられます。使用する言語のフレームワークによっては、文字を出力する際にデフォルトでエスケープ処理を行うものもあり、対策が意識されている攻撃と言えます。ですが、実際に診断している中で、サイト上のごく一部にフレームワークを介さず実装された機能が存在し、そこでXSSの脆弱性が見つかるというようなケースも見受けられます。
コンテンツセキュリティポリシー(CSP)でできること【設定例】
先ほどの例のように
「基本的にXSSの対策は大丈夫なはず。だけど、もし脆弱性があった時の保険になるものがあれば・・・」
そういった時に、CSPが検討できます。
CSPはブラウザが持つセキュリティの仕組みで、設定したルールに沿ってスクリプトの読み込みや実行、<iframe>での呼び出し等に制限をかけることができ、XSSやクリックジャッキング等の攻撃に効果があるとされています。
ではどのような設定ができるのか、具体的にみていきたいと思います。
まずCSPを有効にするためには、サーバーからのレスポンスヘッダに以下のような設定を行います。
Content-Security-Policy: {設定するディレクティブ}
XSSに対する設定を明示的に行う場合、「script-src」が使用するディレクティブになります。
続いて「script-src」で設定できる値について、いくつか紹介していきます。
script-src ‘unsafe-inline’
「unsafe-inline」はイベントハンドラを含むすべてのインラインスクリプトの実行を許可します(図1)。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>
csp test
</title>
</head>
<body>
<script>alert(1);</script>
</body>
</html>
そのため、設定前の状態と同様にインラインスクリプトの実行ができてしまう、穴のある設定と言えます。
script-src ‘unsafe-eval’
CSPは、デフォルトで「eval()」や「Function()」等の文字列をコードとして評価するようなメソッドの実行をブロックします。
参照:安全ではない eval 式
「unsafe-eval」はeval()やFunction()のような文字列をコードとして評価するようなメソッドの実行を許可します(図2)(※)。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>
csp test
</title>
</head>
<body>
<script>alert(eval('1 + 1'));</script>
</body>
</html>
※この例ではインラインスクリプト実行のために、レスポンスヘッダにはscript-src 'unsafe-inline' 'unsafe-eval'
を設定しています。
script-src ‘self’
「self」は同じオリジンから読み込まれたスクリプトのみ実行を許可します(図3)。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>
csp test
</title>
</head>
<body>
<script src="./csp.js"></script>
</body>
</html>
// csp.js
alert(1);
オリジンが異なるスクリプトの読み込み・実行やインラインスクリプトの実行はブロックされます(図4、図5)。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>
csp test
</title>
</head>
<body>
<script src="http://127.0.0.1:8888/csp.js"></script>
<script>alert(1);</script>
</body>
</html>
script-src {host-source}
読み込むスクリプトのホストを
script-src http://127.0.0.1:8888
のようなホワイトリストで許可することもできます(図6)。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>
csp test
</title>
</head>
<body>
<script src="http://127.0.0.1:8888/csp.js"></script>
</body>
</html>
// csp.js
alert(1);
インラインスクリプトや、ホワイトリストの内容と一致しないスクリプトの読み込み・実行はブロックされます(図7、図8)。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>
csp test
</title>
</head>
<body>
<script>alert(1);</script>
<script src="http://127.0.0.1:8080/csp.js"></script>
</body>
</html>
script-src ‘nonce-XXXXXX’
nonceはレスポンスヘッダで返されたnonceと同じnonceが設定されたスクリプトの読み込み・実行を許可します。
nonceの値はリクエスト毎に変化し、推測できない値である必要があります(それに合わせてソース内のnonceの値も動的に変化する必要があります)。
レスポンスで返されるnonceの値が
script-src 'nonce-12345678'
の時の実行結果は以下のようになります(図9)。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>
csp test
</title>
</head>
<body>
<script nonce="12345678">alert(1);</script>
</body>
</html>
スクリプトがレスポンスヘッダで返されるnonceを持たない場合、スクリプトの実行はブロックされます(図10)。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>
csp test
</title>
</head>
<body>
<script>alert(1);</script>
</body>
</html>
script-src ‘nonce-XXXXXX’ ‘strict-dynamic’
strict-dynamicは実行を許可したスクリプトから読み込まれたスクリプトも同様に実行を許可します。
親のスクリプトが得た信頼を、子に継承することを許可するイメージです。
「strict-dynamic」は前述のnonceや、hash-source(本記事ではご紹介しておりません)と合わせて使用する事ができます。
ここではnonceと合わせた際の紹介をします。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>
csp test
</title>
</head>
<body>
<script nonce="12345678" src="./csp2.js"></script>
</body>
</html>
// csp2.js
var elem = document.createElement("script");
elem.src = "./csp3.js";
document.body.appendChild(elem);
// csp3.js
alert("csp3.js is loaded.");
上記のソースは、nonceで許可された「csp2.js」によって、<script src="./csp3.js"></script>
を生成し、生成されたスクリプトで読み込んだ「csp3.js」でアラートが実行されます。
この時、CSPでscript-src 'nonce-12345678'
のみを設定していた場合、「csp2.js」を読み込むスクリプトは同じnonceを持つため、読み込み・実行されます。
一方で「csp2.js」によって生成される<script src="./csp3.js"></script>
は同じnonceを持たないため、CSPの制限を受けエラーになります(図11)。
そこで、「strict-dynamic」を加えレスポンスヘッダでscript-src 'nonce-12345678' 'strict-dynamic'
を設定することにより、同じnonceを持つ「csp2.js」によって生成される<script src="./csp3.js"></script>
に対してもnonceの信頼が継承されるため、読み込んだ「csp3.js」でアラート処理が実行されます(図12)。
このような「nonce」と「strict-dynamic」を組み合わせたCSPの設定を、Googleは「Strict CSP」と呼んでいるようです。
参照:Content Security Policy
CSPでXSSを完全に防ぐことはできるか
このようにCSPを使用することで、設定した内容に応じてスクリプトの読み込み・実行を制限する事ができます。
そこで「CSPさえ有効にしていれば、XSSを完全に防ぐ事ができるのでは?」と思われた方がいらっしゃるかもしれません。
例えば、「script-src ‘self’」を設定したサイトでXSSの脆弱性が存在した場合、
- インラインスクリプトの実行ができない(<script>alert(1);</script>)
- 外部からのスクリプトの読み込みができない(<script src=”{外部のコンテンツ}”>)
上記のようになるため、任意のスクリプトの実行まで繋がってしまう事は無いよう思えます。
しかし、「script-src ‘self’」は同じオリジンから読み込まれるスクリプトの読み込み・実行を許可する設定となるため、
- サイト上に任意のファイルをアップロードできる機能がある
- アップロードされたファイルが同じオリジン上の公開領域に配置される(対象のファイルのパスが分かる)
このような条件を満たした時に、悪意あるファイルをアップロードし、そのファイルを読み込むスクリプトとする事で、読み込まれるスクリプトは「同じオリジン上のもの」となり、設定したCSPの制限内で任意のスクリプトの実行に繋がってしまう可能性があります。
また、URLベースのホワイトリストでの設定においても、許可するホストによっては設定したCSPの制限内で任意のスクリプトの実行に繋がってしまう可能性があります。
「nonce」+「strict-dynamic」の組み合わせにおいても、読み込んでいるJSライブラリによってはCSPの制限内で任意のスクリプトの実行が行える可能性がある等、他の設定も含め、場合によってはCSPの制限を回避できてしまうため、CSPは抜本的な対策とすることは難しいと言えます。
以上を踏まえ、エスケープ・サニタイズなどの一般的なXSS対策を徹底した上で、もしもの時に対するリスク軽減策としてCSPを取り入れる事がベストでしょう。
まとめ
- XSSの脆弱性が存在した際に、CSPによってスクリプトの読み込み・実行に制限をかける事が可能
- CSPによる制限は回避できるケースもあるので、あくまでも「リスク軽減策」と考える必要がある
今回この記事で紹介したCSPの設定は、script-srcディレクティブにおける設定の一例です。CSPには他のディレクティブを含め行える設定がいくつもありますので、この記事をきっかけにCSPをはじめとするブラウザまわりのセキュリティに興味を持っていただけると嬉しいです。
弊社では一緒に働く仲間を募集していますので、もし興味を持たれたエンジニアの方がいらっしゃれば、こちらもご覧ください。
なお、自社のWebサイトの脆弱性を一度全体的に確認したい、と思われた方がいらっしゃるかもしれません。弊社では脆弱性診断サービスがございますので、宜しければご検討ください。