2025年7月23日水曜日

Sinatra で CSP レスポンスヘッダーを設定して XSS 対策する方法

Sinatra で CSP レスポンスヘッダーを設定して XSS 対策する方法

概要

CSP ヘッダを明示しなくても大きな問題にはなりませんが基本的に何でも受け入れる設定なので可能な限り設定したほうがいいかなと思います
そもそも外部にアクセスせずに self のみで完結するのが理想ですがそういうわけにもいかないのでアクセスするべき外部サイトを明示化しておくことでセキュリティ対策になる感じです

今回は Sinatra アプリで CSP (Content Security Policy) レスポンスヘッダーを設定する方法を紹介します

環境

  • Ruby 3.4.4
  • Sinatra 4.1.1

サンプルコード

まずは CSP を管理するクラスを定義します

  • vim libs/csp.rb
# frozen_string_literal: true

# Content-Security-Policy (CSP) の設定を行うクラス
class CSP
  def header
    "#{script_src} #{img_src} #{style_src} #{frame_src} #{connect_src} #{media_src} #{other_src}"
  end

  def script_src # rubocop:disable Metrics/MethodLength
    script_src_url_list = [
      'https://cdn.jsdelivr.net',
      'https://code.jquery.com',
      'https://www.googletagmanager.com',
      'https://cdnjs.cloudflare.com',
      'https://d3js.org',
      'https://unpkg.com',
      'https://www.youtube.com'
    ]
    gtag_hash = "'sha256-3j3z4K5sw7JEbB9oTbDCJixRv+lWuUYVNr8dOyKK7C0='"
    "script-src 'self' #{gtag_hash} #{script_src_url_list.join(' ')};"
  end

  def img_src # rubocop:disable Metrics/MethodLength
    img_src_url_list = [
      'https://lh3.googleusercontent.com',
      'https://blogger.googleusercontent.com',
      'https://addons.mozilla.org',
      'https://pbs.twimg.com',
      'http://pbs.twimg.com',
      'https://avatars.githubusercontent.com',
      'https://c10.patreonusercontent.com'
    ]
    "img-src 'self' data: #{img_src_url_list.join(' ')};"
  end

  def style_src
    style_src_url_list = [
      'https://cdn.jsdelivr.net',
      'https://unpkg.com',
      'https://cdnjs.cloudflare.com',
      'https://cdn.plyr.io/'
    ]
    # public/js/fontawesome-all.min.js が書き換わったら以下のハッシュが変わる可能性もある
    font_awesome_hash = "'sha256-bviLPwiqrYk7TOtr5i2eb7I5exfGcGEvVuxmITyg//c='"
    # views/alchol.erb で使用しているd3.jsのハッシュ
    d3_hash = "'sha256-WrkFMt0yMbnytekpJNs62cGCUpYDzgmKFnWzVnKQ6YY='"
    "style-src 'self' #{font_awesome_hash} #{d3_hash} #{style_src_url_list.join(' ')};"
  end

  def frame_src
    frame_src_url_list = [
      'https://www.youtube.com'
    ]
    "frame-src 'self' #{frame_src_url_list.join(' ')};"
  end

  def connect_src
    connect_src_url_list = [
      'https://cdn.plyr.io'
    ]
    "connect-src 'self' #{connect_src_url_list.join(' ')};"
  end

  def media_src
    media_src_url_list = [
      'https://storage.googleapis.com'
    ]
    "media-src 'self' #{media_src_url_list.join(' ')};"
  end

  def other_src
    [
      "form-action 'self';",
      "frame-ancestors 'self';",
      "font-src 'self';",
      "object-src 'self';",
      "manifest-src 'self';"
    ].join(' ')
  end
end

基本的には各 CSP ヘッダごとに「許可するサイト」「許可するハッシュ」を定義します
許可するサイトは見慣れた CDN サイトが並ぶ感じになります

許可するハッシュはどうしても style タグを使って直接 CSS を指定している場合にだけ使うので CSS ファイルを外部ファイルで管理している場合には不要です

使う側

CSP レスポンスヘッダーを指定するのは before が一番簡単です
特定のパスだけ適用したくない場合などは unless いましょう

before do
  # Content Security Policy (CSP) の設定
  unless request.path.start_with?('/podcast/feed')
    csp = CSP.new.header
    headers 'Content-Security-Policy' => csp
  end
end

動作確認

アプリを起動して問題なくすべてのコンテンツ (js or css or img etc) が取得できることを確認しましょう
Chrome などの開発者ツールでコンソールにエラーがないことを書くにしましょう
CSP は基本的にホワイトリスト形式なので設定が足りていないとコンテンツが取得できずエラーになりうまくサイトが表示できなくなります

基本的には CSP を設定した場合には style タグや onclick タグは使えないと思ったほうがいいです
css ファイルや js ファイルにすべて処理を移す感じになります

最後に

CSP レスポンスヘッダーを正しく設定することで XSS 対策しセキュアコードしましょう

参考サイト

0 件のコメント:

コメントを投稿