Webブラウザのセキュリティについて理解しよう(CSP)
はじめに
はじめまして。大学4年生のとうふです。
今回は、5回目のZenn投稿になります!
本記事は、前回投稿した XSS脆弱性 に関する記事の続編です。
また、Origin / SOP / CORS に関する基礎的な内容も、別記事として公開していますので、まだご覧になっていない方は、以下の記事を先に読んでいただくことで、より本記事の内容が理解しやすくなるかと思います。
今回は、CSP(Content Security Policy) の仕組みと、その重要性について解説します。
前回までと同様、コンパクトに実例を交えて、できるだけわかりやすく解説することを心がけました。
ぜひ最後までお付き合いいただけると嬉しいです!
XSSの復習
XSS(Cross-Site Scripting) とは、
攻撃者が仕込んだ任意のJavaScriptがユーザーのブラウザ上で実行されてしまう、深刻なセキュリティ脆弱性です。
前回の記事では、実際のコード例を交えながら、
XSSがどのように発生し、どのような被害(Cookieの盗難・セッションの乗っ取りなど)を引き起こすのかを詳しく解説しました。
しかし、SOP(Same-Origin Policy) はあくまで「異なるオリジン間の不正アクセス」を制限するルールであり、
Webアプリケーション自体に存在する脆弱性(=XSSやContent Injection)を防ぐことはできません。
実際、SOPでは次のような問題を防げません:
- HTMLやスクリプトに対する Content Injection
- ユーザー入力が埋め込まれて発火する XSS
では、SOPの限界を補うにはどうすればいいのか?
ここで登場するのが、Content Security Policy(CSP) です。
次章では、この CSP がどのように働き、どのように Web アプリケーションを守ってくれるのかを見ていきましょう。
CSP(Content Security Policy)とは
CSP(Content Security Policy) は、Webページ上でどのリソースを読み込んでいいか、実行をしていいかを開発者がブラウザに伝えるための仕組みです。
SOPでは守りきれなかった「同一オリジン内でのスクリプト実行」などに対して、
CSPはより細かく制限をかけて、攻撃を未然に防ぐ役割を担います。
使い方
CSPは、主にHTTPレスポンスヘッダーの Content-Security-Policy
を使って、
Webブラウザに「どのリソースを許可するか」を指示することで動作します。
以下は、最も基本的な指定例です。
Content-Security-Policy: default-src 'self'; script-src 'self';
このヘッダーは、次のような意味を持ちます。
-
default-src 'self'
: 画像、スタイル、フォントなど、すべてのリソースは同一オリジンからの読み込みを許可します。
※img-src
やstyle-src
などが明示的に指定されていない場合には、このdefault-src
の値が適用されます(=フォールバックが起こります) -
`script-src 'self' : JavaScriptファイルも同一オリジンから読み込み可能です
また、代替手段として、HTML内に以下のような<meta>
タグを埋め込んで設定することも可能です。
いくつかのディレクティブについて紹介
ここではいくつかのディレクティブについて、例を挙げながら紹介します。
script-src
script-src
は、<script>タグから読み込むスクリプトや、インラインスクリプトの利用を制限するためのディレクティブです。script-src
もしくはdefault-src
が指定されると、以下のような制約がつきます。
- インラインスクリプトの実行が原則禁止される
- 与えられたソースリストに含まれないJavaScriptのロードが禁止される
script-src 'self' https://cdn.example.com;
この例では、以下のような意味を持ちます。
-
'self'
: 同一オリジンから読み込まれるスクリプトのみを許可 -
https:a.example.com
: このhttps:a.example.com
からのスクリプト読み込みも許可
また、script-src-elem
やscript-src-attr
というディレクティブも存在します。これらはそれぞれ、インラインの<script>タグ、インラインのイベントハンドラに対して個別の制限をかけることができます。
部分的な制約の緩和
すでにあるサイトに新しくCSPを入れる場合などは、インラインスクリプトなどが利用されている部分を全て取り除くことができないことがあると思います。そのような場合、キーワードが含まれている場合は部分的に制約を緩和することが可能になっています。
-
unsafe-eval
: 動的なスクリプト実行を許可する -
unsafe-inline
: インラインスクリプトおよびインラインスタイルの実行を許可する -
unsafe-hashes
: ハッシュ値が「hash-source」としてディレクティブのソースリストに含まれる時、インラインスクリプトおよびインラインスタイルの実行を許可する
img-src
img-src
は、画像の読み込み元を制限するディレクティブです。
Content-Security-Policy: img-src 'self' https://images.example.com;
この例では同一オリジンおよびimages.example.com
からの画像の読み込みを許可します。
connect-src
connect-src
はWebページ中のJavaScriptや<a>タグのping属性により発行されるHTTPリクエストの宛先URLを制限するディレクティブです。具体的にはping属性以外に、Fetch APIなどがあります。
Content-Security-Policy: connect-src 'self' https://api.example.com;
この例では同一オリジンおよびapi.example.com
への通信のみ許可しています。よって、以下のようなJavaScriptによるリクエストの発行ができなくなります。
fetch("http://a.example.com")
frame-src
frame-src
は<iframe>
要素で読み込むコンテンツの読み込み先を制限するディレクティブです。
Content-Security-Policy: frame-src 'self' https://player.example.com;
この例では同一Originおよびhttps://player.example.com
からの読み込みのみ許可しています。
frame-ancestors
frame-ancestors
は現在のページがどのオリジンのページから<iframe>
などで埋め込まれることを許可するかを制限するディレクティブです。
つまり、「自分がどこから埋め込まれても良いか」 を定義します。
Content-Security-Policy: frame-ancestors 'self' https://parent.example.com;
この例では、https://parent.example.com
および同一Originからの埋め込みは許可します。
実際の挙動を確認してみる
ここではframe-src
とframe-ancestors
の違いを理解するために、簡単な例で<iframe>
の埋め込み挙動を確認してみます。
まず以下のような/hello
ページがlocalhost:8787
で動いているとします。
app.get("/hello", (c) => {
return c.html(`
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Hello</title>
</head>
<body>
<h1>Hello World!</h1>
</body>
</html>
`);
});
次に、別のWebアプリ(localhost:3000
)の/admin/user
ページから、このページを<iframe>
を使って埋め込もうとしています。
1. 何も設定していない場合
CSPが何も設定していない場合、以下の画像のようにlocalhost:3000
のページは問題なくlocalhost:8787/hello
を埋め込むことができました。
localhost:3000
側で frame-src
を設定した場合
2. 次に、localhost:3000
で動いているページに frame-src
を設定した場合の挙動を確認します。
比較対象
- ✅
frame-src 'self' http://localhost:8787
:埋め込み成功 - ❌
frame-src 'self'
のみ:埋め込み失敗
frame-src 'self'
のみを指定した場合
❌ この設定では、同一オリジン以外からの埋め込みがブロックされるため、localhost:8787
のページは読み込めません。
Chrome の DevTools では次のようなエラーが表示されました。
Refused to frame 'http://localhost:8787/' because it violates the following Content Security Policy directive: "frame-src 'self'".
frame-src 'self' http://localhost:8787
を指定した場合
✅ このように、許可したいドメインを frame-src
に明示することで、外部ページの埋め込みを許可できます。
このように、frame-src
は 「埋め込む側のページが、どの外部ページを <iframe>
で表示してよいか」 を制御するディレクティブです。
frame-ancestors
の設定による制限
3. 一方で、埋め込まれる側のページが、どのオリジンからの埋め込みを許可するか を指定するのが frame-ancestors
です。
例えば、localhost:8787
側で以下のように設定をします。
app.use(
"*",
cors({
origin: "http://localhost:3000",
allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allowHeaders: ["Content-Type", "Authorization"],
credentials: true,
}),
secureHeaders({
contentSecurityPolicy: {
defaultSrc: ["'self'"],
frameAncestors: ["self"],
},
})
);
このページは 同一オリジン(localhost:8787
)からしか埋め込めなくなり、別オリジン(例:localhost:3000
)からの <iframe>
による埋め込みは拒否されます。
埋め込みを試みると、以下のようなエラーが表示されます。
Refused to frame 'http://localhost:8787/' because an ancestor violates the following Content Security Policy directive: "frame-ancestors self".
次に、frameAncestorsの部分を以下のように変更します。
secureHeaders({
contentSecurityPolicy: {
defaultSrc: ["'self'"],
frameAncestors: ["self", "http://localhost:3000"],
},
})
こうすることでエラーが消え、localhost:8787/hello
のページが正常に表示されました。
このように、frame-ancestors
は 「埋め込まれる側が、どのオリジンからの埋め込みを許可するか」 を明示的に制御するためのディレクティブです。
対照的に frame-src
は、「埋め込む側が、どのリソースを iframe に許可するか」 を制御するものであり、両者は役割が異なります。
つまり:
-
frame-src
は「埋め込み元が、何を埋め込むか」を制御する -
frame-ancestors
は「埋め込まれる側が、誰に埋め込ませるか」を制御する
この2つを適切に組み合わせることで、意図しない埋め込みやクリックジャッキング攻撃などを防ぐことができます。
まとめ
今回は、Content Security Policy(CSP) の基礎から、実際の挙動までを一通り見てきました。
特に重要なポイントは次のとおりです:
- CSPは、SOPでは防ぎきれないXSSやクリックジャッキングを防ぐための追加セキュリティレイヤー
-
script-src
やimg-src
、connect-src
などを使うことで、リソースごとに細かく読み込み元を制御できる -
frame-src
は 埋め込む側が、どのリソースを iframe で読み込めるか を制御する -
frame-ancestors
は 埋め込まれる側が、どのページからの埋め込みを許可するか を制御する
セキュリティ対策は「やったつもり」ではなく、実際に正しく設定し、その挙動を確認することが何よりも重要だと、今回の検証を通じて改めて実感しました。
今回の記事では動作環境の詳細な構築方法までは説明していませんが、
2つのページを立ち上げるだけで、簡単にCSPの挙動を確認する環境は作れると思います。
もし興味があれば、ぜひご自身でも試してみてください。
CSPの設定によって埋め込みの挙動がどのように変化するかを実際に確認することで、理解がより深まるはずです。
なお、CORSなどの設定が必要になる場面があれば、冒頭で紹介した私の記事も参考になるかと思います。
手を動かして試すことが、最も効果的な学びにつながります。
本記事が、皆さんのWebアプリにCSPを導入するきっかけや参考になれば幸いです。
最後までお読みいただき、ありがとうございました!
Discussion