Classi開発者ブログ

教育プラットフォーム「Classi」を開発・運営するClassi株式会社の開発者ブログです。

Content Security Policy のレポートを収集するためにやったこと

はじめに

こんにちは、開発本部所属エンジニアの id:kiryuanzu です。 現在、Classi ではサービスのセキュリティリスクをできる限りなくすために Content Security Policy を導入して脆弱性を検知する仕組みの導入を進めています。

本記事ではこの仕組みを導入する上でどのような手順が必要であり、どのような箇所で苦戦するポイントがあったかについて紹介していきます。

筆者は今まで CSP対応に携わったことがなかったのですが、導入段階の時点で想定していたよりも様々な知識が必要なことがわかり、記事にしたいと思いました。 もし数ヶ月前の自分と同じように初めてCSP対応に関わる人の一助となれば幸いです。

Content Security Policy (通称: CSP) って何?

Content Security Policy とは、HTTPヘッダの種類の1つであり、クロスサイトスクリプティング(XSS) やデータインジェクション攻撃といった、特定の種類の攻撃を検知し影響を軽減するために追加できるセキュリティレイヤーです。 MDN のドキュメント内に詳細な記事が存在するため、より具体的に知りたい方はそちらの記事も参照ください。

developer.mozilla.org

CSP導入の方針

今回 Classi で CSP対応を導入することになった経緯としては、EOL を迎えたフロントエンドライブラリが含まれており、なおかつクローズ時期が迫ったサービスに対して、バージョンアップの対策を取らない代わりにCSPを導入して脆弱性が発生した時にすぐ検知できるようにすることになったためです。クローズ時期が近いサービスに対してバージョンアップ対応に時間を割くよりも、CSP対応を行ってEOLオーバーを許容する環境を用意することで運用コストを低くかつEOLによるリスクを抑えられると判断し、導入を決定しました。

まずは上記のような緊急性の高いサービスを優先してCSP対応を行い、バージョンアップを定期的に行いクローズ時期の予定がないサービスに対しても順次CSPを導入していくことにしました。

今回 CSP 対応をする中での基本方針は以下の通りでした。

  • Content Security Policy Report-Only ヘッダを使うことで違反内容の検知をレポートだけにとどめる
  • できる限り厳格な規格に沿ってポリシーを設定する
  • セキュリティ部署との密な連携

方針に対して具体的にどのような取り組みを行ったかひとつずつ紹介していきます。

Content Security Policy Report-Only ヘッダを使うことで違反内容の検知をレポートだけにとどめる

CSP には2種類のヘッダがあり、 1つ目は Content-Security-Policy ヘッダです。このヘッダを設定するとポリシーの違反内容に引っかかった時に違反内容をコンソール上に表示し、サイトのブロッキングまで行います。

2つ目は Content Security Policy Report-Only ヘッダがあります。このヘッダはサイトのブロッキングはせず違反内容のレポートのみになります。

もし Content Security Policy ヘッダを本番環境で設定した場合、想定しない場面でサイトがブロックされユーザーに不便が生じるリスクがあります。このリスクを回避するため、まずは staging・production 環境においてContent Security Policy ヘッダを設定しレポート系のサービスに収集させることにしました。

Content-Security-Policy-Report-Only の report-uri ポリシーを使って Sentry などのバグ収集サービスと連携させることで、CSPが検知した違反内容を Sentry に飛ばしイシューとして管理・Sentry に積まれた違反レポート内容を slack 連携してチャンネルに流す運用を今回行いました。

docs.sentry.io

できる限り厳格な規格に沿ってポリシーを設定する

CSP では以下のようにポリシーを指定して特定の記述やオリジンを許可したりブロックしたりできます。

Content-Security-Policy: default-src ‘self’; script-src example.com ‘sha256-xxx’’; style-src: example2.com ‘sha256-yyy’

特定のインラインコードやインラインスタイルを許可したい時はハッシュ値や nonce (一時的に利用する乱数のこと)を設定して対応するように進めました。もしひとつひとつ許可する方法に対してコストを高く感じる場合は、unsafe-inlineunsafe-styleを各ポリシーに設定することで許可できます。しかし、意図していないコードの実行を許可してしまうことに繋がるためこれらの設定は極力使わないようにしました。

CSP の規格として現在推奨されている規格として CSP Level 3 といった内容が存在しています。

w3c.github.io

この Level 3 は今まで示されていた Level 2 の対策よりもよりセキュアな仕様とされています。CSP Level2 から Level3 における XSS対策は pixiv さんの記事で詳しく紹介されているので、気になる方はぜひ読んでみてください。(今回のCSP対応を進める中でも大変参考になりました)

inside.pixiv.blog

セキュリティ部署との密な情報連携

対応を進める中で、現状のサービス設計だと厳格なポリシー設定を守れないケースが出てきたり、 CSP 違反で検知されたコードの調査が難しい場面がありました。 そういった問題に対しては、セキュリティ部署のエンジニアと一緒に現状の設定でセキュリティリスクがないかどうかや違反されたレポートが無害であるかどうか相談をする場を設けて現状の運用に対しての合意を取るように進めました。

CSP対応を進めるうえで必要だった実作業の紹介

初めてCSP対応を進める上でキャッチアップが必要かつ時間をかけて考える必要のあった作業を紹介していきます。

サービスの技術セットに合わせてヘッダの設定場所を調査する

実装に手をつける前に、まずはCSPのヘッダを設定できる場所を調査する作業から始めました。

Report-Only ではない方のCSPは HTML の metaタグを設定するだけでCSPの設定を適用できますが、Report-Only では対応されていません。サーバー側でレスポンスヘッダを設定する方法で対応を進める必要があります。

この方法で対応する場合、Rails 等のフレームワーク内の標準機能を利用する方法や nginx などのリバースプロキシ上で設定する方法など、サービスの技術セットに応じて設定する箇所を見つける必要がありました。

1つ例を紹介すると、Rails では、CSP を設定するための DSL が提供されています。 railsguides.jp

今回 CSP対応を行ったサービスの1つは、開発上の都合で S3に用意したフロント側のファイルを public 以下に配置する設計となっており、Railsは静的ファイルを配信するサーバーとして使われている構成だったため、上記の DSL は利用しませんでした。(なぜこうなっているかは CSP の文脈から逸れてしまうので割愛します)

application.rb 内で config.public_file_server.headers の設定を利用することで CSP のレスポンスヘッダを設定しました。 この設定は Cache-Control 等の他のヘッダを指定したい時にも使われています。

railsguides.jp

# application.rb

# CSP記述箇所以外は省略
    config.public_file_server.headers = {
      'Content-Security-Policy-Report-Only' => "default-src 'self';

このようにしてサービスの設計に合わせて設置場所を定めるための調査をする必要がありました。

その中で、設置場所によっては容易にできる実装だったものが自力で用意しなければならない局面があり、少し時間をかけてしまいました。 次のパートで詳しく紹介していきます。

特定のスクリプトに値を動的に変更する nonce の設置対応

Level3 の仕様の中では以下のコードのように、 nonce を使って javaScript の実行を制限する方法が推奨されています。

# index.html

<script nonce="hogehoge111">
  alert(“piyo”)
</script>
# nginx.conf

add_header Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self' 'nonce-hogehoge111'

nonce とは「一回限り有効なランダムなデータ。リプレイ攻撃などを防ぐために用いる」といった意味の単語です。CSP ではこの nonce を固定値のまま設定して制御する方法も可能ですが、その方法だとポリシーの設定によっては nonce の値を攻撃者から悪用されるリスクもあるため、リクエストごとに変更する設定にしました。

特定のスクリプトに nonce を設置する際、前者で紹介した DSL では用意された設定を用いることで容易に nonce の自動生成を行うことができます。(具体的な設定方法は https://railsguides.jp/security.html#content-security-policy%EF%BC%88csp%EF%BC%89を参照してください)

しかし、Rails の public_server を使った設定や nginx で設定する場合はこちらで nonce を動的に変更にするための実装を考える必要がありました。

nginx で nonce の値を動的に変更できる対応を行うために、 ngx_http_sub_module というライブラリに用意された sub_filter メソッドを利用してフロントエンドのコードに 仮の nonce の値(placeholder)を埋め込んで nginx の設定の中で placeholder の値からリクエストID に置き換えることでリクエストごとに nonce を用意しました。

Module ngx_http_sub_module

Easy nonce-based Content-Security-Policy with Nginx | Infrastructure and Application Security

# index.html

# placeholder の値を CSP_NONCE にしている
<script nonce="CSP_NONCE">
  alert(“piyo”)
</script>
# nginx.conf

load_module /usr/lib64/nginx/modules/ngx_http_opentracing_module.so;
opentracing on;
# リクエストIDを変数で使えるように設定
opentracing_tag nginx_request_id $request_id;
opentracing_trace_locations off;

load_module  /usr/lib64/nginx/modules/ngx_http_subs_filter_module.so;
sub_filter_once off;
# フロント側でplaceholder の値として設定した CSP_NONCE 文字列をリクエストIDに置き換えて nonce の値として使うように
sub_filter CSP_NONCE $request_id;
add_header Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self' 'nonce-$request_id'

placeholder の値(ここではCSP_NONCEの文字列として記述)自体も、固定値のままにしておくとバレてしまった際に攻撃者から利用されるリスクがあるので、できるだけ動的に変更にした方が良いというアドバイスを社内でいただきました。

そのため、CircleCI の中で placeholder にあたる値を乱数に置換するコマンドを用意し、nginx.conf 内にある placeholder も置換するようにしてデプロイごとに変更する仕組みを用意しました。

# .circleci/config.yml

  set_nonce:
    steps:
      - run:
          name: Set NONCE
          shell: /bin/bash
          command: |
            nonce=$(cat /dev/urandom | base64 | fold -w 16 | head -n 1)
            sed -i "s@CSP_NONCE@$nonce@g" src/index.html
            echo "export NONCE=${nonce}" >> $BASH_ENV
      - run: echo ${NONCE}
# *この設定は必要最低限抽出したもので、実際には他の引数やパラメータも用意してビルドしています
jobs:
   image-build-and-push:
     steps:
       - checkout
       - set_nonce
       - aws-ecr/build-and-push-image:
         repo: xxx
         dockerfile: docker/frontend/Dockerfile
         extra-build-args: "--ssh default --build-arg NONCE=$NONCE"

ビルド時に CircleCI 側で用意した環境変数を Dockerfile の中で 置換する処理を入れ、デプロイ時に placeholder を書き換える処理にしました。

# nginx.conf

ARG NONCE
RUN sed -i "s|CSP_NONCE|${NONCE}|g" /etc/nginx/nginx.conf

このようにして、nginx 内の設定でも nonce の値を動的に変更するように設定し、よりセキュアなやり方で nonce方式でCSPを運用できるようにしました。

ただし、上述で紹介した Rails の config.public_file_server.headers の設定では自力で実装することが難しかったため、先ほどのコードで示したような nonce の値を CircleCI の中で置換する仕組みを用いて nonce 自体をデプロイごとで動的に変更できる設定を行うことにしました。

この対応は厳格なやり方から外れてしまいますが、他のポリシーで必要最低限の脆弱性から守るようにすることをセキュリティ部署のメンバーと合意し運用するようにしました。

CSP のポリシーの設定・運用フェーズへの体制を整える

上記のような nonce 対応も行う必要がありますが、基本的には実際に CSP のポリシーをサービスに適用するうえで現状の違反内容をチェックし、1つずつ解消していく作業をやっていきました。

ポリシーをdefault-src: selfのみ適用することで、現時点で存在する違反内容がコンソール上で表示されるため、この違反内容を1つずつ精査し、特定のハッシュ値や nonce の適用・不要なインラインスクリプトの削除を進めました。

この作業の中を進める上で、特定のスクリプトに対応するハッシュ値を記録するドキュメントを残して対応するスクリプトが不要になった時は削除できるように管理しています。 特に大変だったこととして、動作する環境やブラウザによって発見されるレポートが変わることが多々あり、原因を調査をしていくのにかなり時間がかかりました。

今は対処できる違反レポートを一通り対処し終わった段階で、Sentry のレポートを slack に流す連携を入れ、社内ドキュメントに運用ルールを記載し、トリアージやエスカレーションの方法を取り決めて運用体制を整えました。

まとめ

上記のような導入作業を終わらせてCSPのレポートを運用する状態に持っていくことができました。ただ、導入してからこそが本番でこのレポートたちの情報を元に傾向をまとめたり必要な対策を実施していく必要があると考えています。

jxck さんが執筆された CSP Report 収集と実レポートの考察 | blog.jxck.io のような活動を社内のレポートを元に行っていくのが次の目標です。

今後CSP対応に関わる方に参考になる話があれば幸いです。読んでいただきありがとうございました。

© 2020 Classi Corp.