複雑な環境における Content-Security-Policy
このブログはこちらの英語ブログ(2025年2月19日公開)の機械翻訳です。
Content-Security-Policy(CSP)は、Webサイトが読み込み、実行できるコンテンツに関するポリシーのセットを定義し、適用することで、さまざまなサイバーセキュリティ攻撃から保護するウェブセキュリティメカニズムです。
本質的には、Webページが読み込み可能なものを規定する許可リストポリシーです。 CSPの実装と展開は複雑であり、些細なミスでもページの重要な部分が読み込まれないことになり、Oktaの場合、認証に問題が生じる可能性があります。 本ブログ記事では、当社のセキュアな実装の取り組みと、そこから得た教訓に基づく業界向けの指針について紹介します。
OktaはWebセキュリティを重視しています
Oktaは、入力検証や出力エンコーディングなど、クロスサイトスクリプティング(XSS)攻撃に対するさまざまな防御策を採用し、新たな脅威に対するセキュリティ保証の向上に努めています。MITREとCISAがXSSを2024年の最大の脅威として確認したことから、アプリケーションがXSSに対して脆弱である可能性は高いと考えられます。Content-Security-Policyは、ブラウザに対して、どのスクリプトやコンテンツのソースが安全で信頼でき、実行できるかを指示する、事実上のゲートキーパーです。Oktaの環境は複雑であるため、当社のCSPヘッダーは常に改善が加えられています。
実装上の課題
Okta Secure Identity Commitment(OSIC)の主要な柱の1つは、業界の水準を向上させることです。本ブログでは、当社が業界から学んだこと、ヒント、コツなどを共有します。OktaでCSPポリシーを設定するにあたり、セキュリティエンジニアリングチームは実装全体を通して、以下の課題に直面しました。
複雑性
Okta Workforce Identityの性質上、複数のアプリケーション接続、さまざまな機能の組み合わせ、そしてOkta管理者によってカスタマイズ可能なHTMLページが存在する環境で動作します。 基本的なContent-Security-Policyの構築と展開でさえも困難であることが明らかです。
設定上の課題
CSP構成で最も重要な決定事項は、問題のアプリケーションが、Okta管理者がカスタマイズ可能なコンテンツを含むエンドポイントを返すかどうかです。 CSPの構成に関する当社の推奨事項として、以下の 3 つの詳細なアプローチがあります。
1. インターセプターアプローチ
一般的なアプローチは、CSPヘッダーを追加するためにインターセプターを使用することです。 当社では、当初この方法を採用しました。 しかし、この方法にはいくつかの課題があります。
- ユーザーカスタマイズされたHTMLコンテンツを返すエンドポイントに適切なポリシーを追加すること、
- インターセプターの順序に注意すること:CSPヘッダーは、インターセプターの順序の最初の方に追加する必要があります。これは、一部のインターセプターがインターセプターチェーンを早期に中断する可能性があるためです。
- preHandle()とpostHandle()は、コンテンツタイプがHTMLであるかどうかを常に把握しているわけではありませんが、エンドポイントレベルでアノテーションを使用して応答タイプを決定することで、これを示すことができます。
- preHandle() におけるXHR 経由のレスポンスのコミットメントは、postHandle() が実行されると変更できない
- インターセプターにおけるデータベース呼び出しはキャッシュされる可能性があり、レスポンスのコミットは afterCompletion() の後に実行される
2. フィルターアプローチ
アプリケーションがHTMLコンテンツを返す際にCSPヘッダーを追加する代替策として、エンドポイントのコンテンツタイプが判明した時点でヘッダーを適用するという方法があります。以下の例では、HTMLに対してのみCSPを適用するために返されるcontent-typeヘッダーに基づいて、CSPを生成する方法を、Spring Webアプリケーションのフィルターとして表示しています。
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
HttpServletResponseWrapper wrapper = new HttpServletResponseWrapper(response) {
@Override
public void addHeader(String name, String value) {
super.addHeader(name, value);
if (HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(name)) {
setCSPHeaders(value);
}
}
@Override
public void setHeader(String name, String value) {
super.setHeader(name, value);
if (HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(name)) {
setCSPHeaders(value);
}
}
@Override
public void setContentType(String type) {
super.setContentType(type);
setCSPHeaders(type);
}
private void setCSPHeaders(String contentType) {
if (StringUtils.isNotEmpty(contentType) && StringUtils.containsIgnoreCase(type, MimeTypeUtils.TEXT_HTML_VALUE) {
LOG.debug("Content-Type header={}", contentType)
// the code to build the Content-Security-Policy-Report-Only and Content-Security-Policy headers
}
}
};
filterChain.doFilter(request, wrapper);
}
3. エッジアプローチ
スコット・ヘルム氏は、ブログ記事でこのアプローチについて詳しく説明しています。ここでは、HTTPトラフィックを傍受してCSPポリシーを注入するためにエッジを活用しています。同氏の例では、一般的なCSPポリシーを実装する「簡単な」方法として、Cloudflareのサービスワーカーを使用しています。この方法の利点の1つは、複数のアプリケーションに適用できるため、CSPポリシーを維持するためのメンテナンスポイントが1つで済むことです。この方法は汎用的ですが、アプリケーションのコードを変更することなく、どのアプリケーションにも追加できます。
設定に関する考慮事項
違反レポート
Oktaのアプリケーションの範囲は非常に広く、エンドポイントも多数あるため、違反レポートが大量に発生します。 CSPがブラウザに違反レポートを転送するように指示するレポートURIは、すぐに大量のデータを生成します。また、ページロードごとに同じ違反が何度も繰り返されることがほとんどです。 レポートベンダーは、ポリシーに違反するはずのない特定の違反を無視する方法を持っているかもしれません。 レポートベンダーはレポートを受け取り、処理しなければならないため、機能を無視するインセンティブはありません。 この問題に対処するためにOktaが採用した方法は、CSPヘッダーが追加されるサーバー側で違反レポートのサンプリングを実装することでした。CSPヘッダーを受け取るリクエストを制御する機能を提供し、最終的に当社のレポートエンドポイントに送信されるトラフィックを削減しました。この方法の代替案としては、レポートデータを受け取り、繰り返される違反を送信側で無効にする社内ソリューションを構築することも考えられます。
違反の例:Content-Security-Policy(CSP)が介入しなかった場合:
{
"csp-report": {
"effective-directive": "connect-src",
"original-policy": [truncated]
"blocked-uri": "https://mail.google.com/mail/feed/atom/",
"source-file": "user-script",
"line-number": 5,
"column-number": 16842
}
}
違反の例:Content-Security-Policy (CSP) が介入した場合:
Content-Security-Policy(CSP)は、report-uri から当社に報告されたこのスクリプトのような既知の悪意のあるスクリプトを確実にブロックします。以下の例は、CSPによる遮断の抜粋を示しており、環境のセキュリティ保証の姿勢を強化するものです。
{
"csp-report": {
"effective-directive": "script-src-elem",
"Original-policy": [truncated]
"blocked-uri": "https://3001.scriptcdn.net/code/static/1",
"line-number": 7,
"column-number": 47,
"status-code": 200
}
}
内部転送リクエスト
リクエストが内部で転送される場合、リクエストはインターセプターチェーンを複数回通過することがあります。CSPヘッダーが毎回計算される場合、特に計算されたヘッダーが変更されたり削除されたりする場合、これは複雑な問題を引き起こす可能性があります。なぜなら、一度設定されたヘッダーは解除できないからです。管理者がカスタマイズできるページが、カスタマイズできないページに転送される場合、またはその逆の場合に問題が発生します。それぞれのページで異なる CSPが必要になるためです。私たちの使用事例では、転送により誤って計算されたポリシーを元に戻す方法として、frame-ancestorsを含む基本ポリシーを展開しました。
テスト
特定のお客様に対して特定のポリシーを設定し、リアルタイムでのテストとデバッグを行うために、私たちはJava Management Extensions(JMX)を活用しています。これは、お客様からの問い合わせ中に、アプリケーションでCSPポリシーをリアルタイムに変更できる機能を提供してくれるからです。
また、Seleniumテストが非常に重要であると考えています。このテストを行わないと、予期せぬタイミングで本番環境で何かが壊れる可能性が高くなります。私たちは、SeleniumテストのブラウザコンソールにCSPエラーが存在する場合に、Seleniumテストを失敗させることができるフレームワークを構築しました。
カスタマイズされたコンテンツ
OktaのAdmin Consoleのユーザーインターフェース(UI)では、管理者はカスタマイズ可能なページのContent-Security-Policy(CSP)をカスタマイズできます。これにより、カスタマイズを実行し、Content-Security-Policy-Report-OnlyとContent-Security-Policyの間でポリシーを切り替えるCSPの作成が促進されます。また、ブラウザベースの違反レポート用の独自のレポートURLを指定するオプションもあります。
指令
「frame-ancestors」などのナビゲーションディレクティブは、悪意のある攻撃者がHTMLコンテンツだけでなくAPIもiframe化しようとするのを防ぐために、常に追加する必要があります。レスポンスのヘッダーサイズが大きくなることで、非HTMLコンテンツがネットワークトラフィックの増加を引き起こすというデメリットを考慮し、HTMLコンテンツを返すエンドポイントに対してのみフェッチディレクティブを使用することをお勧めします。
ヘッダーサイズの制限
AWS API、Google Cloud Apigee API、Kong API GatewayなどのAPIゲートウェイは、いずれもレスポンスヘッダーサイズに4KBから128KBの制限があります。 また、unsafe-inlineに対処するためにnonceが導入されたことで、レスポンスヘッダーサイズは4KBを超える可能性があります。 APIゲートウェイを使用しているお客様は、より大きなレスポンスサイズを許可するために特別な考慮が必要です。
展開における課題
私たちが学んだ教訓を共有するために、以下のサブセクションでは、上記に挙げた3つの構成方法それぞれにおいて遭遇したロールアウトの課題を説明します。
Unsafe-eval
Unsafe-evalへの対処は、テンプレート化されていない新しいコードを阻止する第一歩です。次のステップは、既存の違反をそれぞれ追跡し、lintingの許可リストからすべての例外を削除することです。最後のステップは、ポリシーからunsafe-evalキーワードを削除することです。
Unsafe-inline
ポリシーから unsafe-inlineを削除すると、大きな影響が予想されます。 削除の主なリスクとして、ポリシーが正しくない場合にユーザーに影響が及ぶ可能性が高いことが挙げられます。 影響としては、ページ上のインラインスクリプトやインラインスタイルがブロックされ、ユーザーエクスペリエンス(UX)が低下したり、ページが適切に読み込まれなくなることが考えられます。インラインスクリプトやインラインスタイルに unsafe-inlineを必要とするサードパーティの統合については、ベンダーにコードを修正して unsafe-inlineを必要としないように依頼するのが最善の策です。そうでないと、継承された不適切な実装に悩まされることになります。PendoやMapboxなどの統合が必要な場合、ベンダーが修正を実装してunsafe-inline/unsafe-evalの要件を削除するには時間がかかる場合があります。
私たちが選んだアプローチは、Content-Security-Policy-Report-OnlyとContent-Security-Policyの両方に対して、スクリプトsrcとスタイルsrcにnonceを追加するかどうかを制御するアノテーションを使用して、各チームがエンドポイントを展開できるようにすることでした。以下のCSPの例は時期尚早かもしれませんが、個別にテストするリスクは低くなります。
@RequestMapping(value = “/api/v1/object, method = RequestMethod.GET)
@ScriptSrcNonce(policy = {ScriptSrcNoncePolicy.SCRIPT_SRC_NONCE_REPORT_ONLY, ScriptSrcNoncePolicy.SCRIPT_SRC_NONCE_ENFORCED}, switchProperty = "team.<name>.<endpoint>kill.switch.enableScriptSrcNonce")
public String listObjectProperties(ModelMap model) {
…
}
推奨事項
CSPのさまざまな部分を制御するために機能フラグを使用し、デプロイ時にさらに制御を行うためにクラウド構成ノブを使用することをお勧めします。展開時には、継続的インテグレーション(CI)を有効にしたり、開発環境を使用したり、前述のJMXを使用して特定の顧客構成でライブテストを実施したりするなど、ガードレールを設置して、よりゆっくりとしたペースで進めることをお勧めします。監視時には、必要に応じてポリシーを徐々に調整し、繰り返し適用することをお勧めします。ほとんどの展開計画と同様に、ポリシーを展開するには、初期展開で最も大きな影響を受ける顧客に焦点を当て、その後、顧客と緊密に連携しながら問題が発生した場合はその都度デバッグを行い、時間をかけてポリシーを改善していくことをお勧めします。
最後に、CSPを安定した状態にロールバックして修正する準備をしておくことをお勧めします。前述のとおり、機能フラグやクラウド構成などの設定は、ロールバックして稼働中の機能状態に戻す際に非常に重要です。CSPは継続的な改善のためにリアルタイムで評価することができます。
結論
結局のところ、Content-Security-Policyの実装はそれだけの価値があるのでしょうか? 当社のセキュリティチームは、「はい!」と答えます。
最も深刻なセキュリティ脆弱性であるクロスサイトスクリプティング(XSS)は、最新のWebアプリケーションにおけるものであり、強力なCSPによってフレームワークレベルで対策されています。 今日の進化する脅威に対する長期的な備えとして、より強固なセキュリティを追加するために導入されています。 その重要性から、カスタムドメインの要望も増えています。Oktaでは、引き続きセキュリティ調査、強化、監視を行い、お客様のデータを保護する努力を続けています。
以上の内容は、原文(英語)の機械翻訳であり、原文と内容に差異がある場合は、原文が優先されます。