# CORSとは？仕組みとプリフライト、危ない設定と安全な書き方

CORS（オリジン間リソース共有）の仕組み、単純リクエストとプリフライトの違い、資格情報付きリクエストの制約、オリジンの反射やnullの許可といった危ない設定を、MDN・OWASP・PortSwiggerの資料をもとにWeb開発者向けに解説します。

CORS（Cross-Origin Resource Sharing、オリジン間リソース共有）は、別のオリジンから読み込まれたページのスクリプトに、自分のサーバーのレスポンスを読ませてよいかをHTTPヘッダーで伝える仕組みです。ブラウザの同一オリジンポリシーを、サーバーが明示した範囲だけゆるめます。CORSは守りを強める仕組みではなく、制限を外す仕組みなので、許可する範囲を広げすぎると情報を読み取られる原因になります。

<Points>

- CORSはサーバーが「どのオリジンになら読ませてよいか」をブラウザに伝える仕組みで、制限をゆるめる方向にしか働かない
- Cookieなどの資格情報を伴うリクエストでは、`Access-Control-Allow-Origin` に `*` を使えず、明示したオリジンが必要になる
- 危ないのは、受け取った `Origin` をそのまま返す設定、`null` の許可、前方一致や後方一致だけの判定。許可リストと完全一致で判定する

</Points>

## CORSとは

オリジンは、スキーム（`https` など）、ホスト名、ポート番号の組み合わせです。MDNによると、`fetch()` や `XMLHttpRequest` は同一オリジンポリシーに従い、他のオリジンのレスポンスは、そのレスポンスに適切なCORSヘッダーが含まれている場合にしか読み取れません。

たとえば `https://app.example.com` のページから `https://api.example.com` のAPIを呼ぶと、ホスト名が違うので別オリジンです。APIのレスポンスに次のヘッダーがあれば、ブラウザはスクリプトにレスポンスを渡します。

```http
Access-Control-Allow-Origin: https://app.example.com
```

<Compare>
<Column title="CORSがしていること" tone="accent">

- 他のオリジンのスクリプトに、レスポンスの読み取りを許可する
- 許可する範囲はサーバーがヘッダーで決める
- 判定して止めるのはブラウザ

</Column>
<Column title="CORSがしていないこと">

- サーバーへのリクエストそのものを止める
- ブラウザ以外（curlやスクリプト）からのアクセスを止める
- 認証や認可の代わりになる

</Column>
</Compare>

OWASPのHTML5セキュリティチートシートは、CORSはCSRFを防ぐものではないとして、CSRF対策は別に必要だと注意しています。CSRFの対策は[CSRFの解説](/csrf/)にまとめています。

## 単純リクエストとプリフライト

CORSのリクエストは、ブラウザが事前確認をせずに送る「単純リクエスト」と、先に `OPTIONS` メソッドで確認する「プリフライトリクエスト」に分かれます。MDNが示す単純リクエストの主な条件は次のとおりです。

<Figure title="単純リクエストの主な条件" source="MDN「オリジン間リソース共有 (CORS)」">

| 項目 | 条件 |
| --- | --- |
| メソッド | `GET`、`HEAD`、`POST` のどれか |
| 手動で設定するヘッダー | `Accept`、`Accept-Language`、`Content-Language`、`Content-Type`、`Range`（単一の範囲のみ）だけ |
| `Content-Type` | `application/x-www-form-urlencoded`、`multipart/form-data`、`text/plain` のどれか |

</Figure>

JSONを `Content-Type: application/json` で送るリクエストや、`Authorization` などの独自ヘッダーを付けたリクエストは、単純リクエストの条件を満たさないのでプリフライトが送られます。

<Flow
  title="プリフライトが行われる流れ"
  steps={[
    { label: "ブラウザがOPTIONSを送る", note: "Access-Control-Request-Method と Request-Headers で予定を伝える" },
    { label: "サーバーが許可内容を返す", note: "Allow-Origin、Allow-Methods、Allow-Headers" },
    { label: "ブラウザが許可を確認する", note: "条件に合わなければ本番のリクエストを送らない" },
    { label: "本番のリクエストを送る", note: "レスポンスにも Allow-Origin が必要" },
  ]}
/>

プリフライトへのレスポンスの例です。`Access-Control-Max-Age` は、プリフライトの結果をキャッシュしてよい秒数です。MDNによると、指定しない場合の既定値は5秒で、ブラウザごとの上限を超える値を指定すると上限が優先されます。

```http
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type
Access-Control-Max-Age: 86400
Vary: Origin
```

<Note tone="caution">

単純リクエストはプリフライトなしでサーバーに届きます。CORSでレスポンスを読めなくしても、リクエストの処理（データの更新など）は実行されます。OWASPも、プリフライトが行われない場合があるので、`GET` や `POST` の処理側で必要なアクセス制御を行うよう求めています。

</Note>

## 資格情報を伴うリクエストの制約

Cookieなどの資格情報は、既定ではオリジン間のリクエストで送られません。送るには、`fetch()` なら `credentials: "include"`、`XMLHttpRequest` なら `withCredentials = true` を指定します。

```js
// フロントエンド：Cookieを付けて別オリジンのAPIを呼ぶ
const res = await fetch("https://api.example.com/me", {
  credentials: "include",
});
```

このときサーバーは `Access-Control-Allow-Credentials: true` を返す必要があり、返さない場合、ブラウザはレスポンスをスクリプトに渡しません。さらにMDNは、資格情報付きのリクエストに応答するときは次のヘッダーで `*` を使ってはならないとしています。

<Figure title="資格情報付きリクエストで * を使えないヘッダー" source="MDN「オリジン間リソース共有 (CORS)」">

| ヘッダー | 代わりに指定するもの |
| --- | --- |
| `Access-Control-Allow-Origin` | 明示したオリジン |
| `Access-Control-Allow-Headers` | ヘッダー名の一覧 |
| `Access-Control-Allow-Methods` | メソッド名の一覧 |
| `Access-Control-Expose-Headers` | ヘッダー名の一覧 |

</Figure>

この制約があるため、「どのオリジンからでもCookie付きで読めるようにしたい」と考えて、受け取った `Origin` をそのまま返す実装が生まれがちです。これが次に挙げる危ない設定の代表です。

## 危ないCORS設定

OWASP Top 10:2025のA01「Broken Access Control」は、CORSの設定ミスによって、許可されていない、または信頼できないオリジンからAPIにアクセスできてしまうことを例に挙げています。PortSwiggerのWeb Security Academyは、よくある設定ミスとして次を挙げています。

<Figure title="よくあるCORSの設定ミス" source="PortSwigger Web Security Academy「Cross-origin resource sharing (CORS)」">

| 設定ミス | 何が起きるか |
| --- | --- |
| `Origin` をそのまま `Access-Control-Allow-Origin` に返す | どのドメインからでもレスポンスを読める |
| `null` オリジンを許可する | サンドボックス化したiframeなどから `null` を送られ、許可を満たされる |
| 前方一致・後方一致・正規表現での判定ミス | 攻撃者が取得した似たドメインが許可される |
| XSSのあるサブドメインやHTTPのオリジンを信頼する | そのオリジンを足がかりに認証付きで読み取られる |
| 社内向けサイトで `*` を返す | 社外のページが利用者のブラウザ経由で社内の情報を読む |

</Figure>

たとえば、Cookieで認証するAPIが次のように応答していると、攻撃者のサイトに誘導されたログイン中の利用者のブラウザから、そのAPIのレスポンスを読み取られます。

```js
// 危険な例：受け取ったOriginをそのまま返している
app.use((req, res, next) => {
  res.setHeader("Access-Control-Allow-Origin", req.headers.origin);
  res.setHeader("Access-Control-Allow-Credentials", "true");
  next();
});
```

文字列の一部だけを確かめる判定も同じように危険です。`origin.endsWith("example.com")` は `attacker-example.com` を通し、`origin.startsWith("https://example.com")` は `https://example.com.attacker.net` を通します。

## 安全な設定の書き方

許可するオリジンを一覧で持ち、受け取った `Origin` と完全一致したときだけ返します。MDNは、オリジンによって返す値を変える場合は `Vary: Origin` を付けるよう説明しています。付けないと、キャッシュが別のオリジン向けのレスポンスを返してしまうおそれがあります。

```js
// 安全な例：許可リストと完全一致したオリジンだけを返す
const allowedOrigins = new Set([
  "https://app.example.com",
  "https://admin.example.com",
]);

app.use((req, res, next) => {
  const origin = req.headers.origin;
  res.append("Vary", "Origin");

  if (origin && allowedOrigins.has(origin)) {
    res.setHeader("Access-Control-Allow-Origin", origin);
    res.setHeader("Access-Control-Allow-Credentials", "true");
  }

  if (req.method === "OPTIONS") {
    res.setHeader("Access-Control-Allow-Methods", "GET, POST");
    res.setHeader("Access-Control-Allow-Headers", "Content-Type");
    return res.sendStatus(204);
  }

  next();
});
```

<Steps>
<Step title="CORSが本当に必要か確かめる">

OWASP Top 10:2025のA01は、対策としてCORSの利用を最小限にすることを挙げています。同じオリジンにAPIを置ける構成なら、CORSを使わずに済みます。

</Step>
<Step title="許可するオリジンを一覧にする">

OWASPは、`Access-Control-Allow-Origin` には信頼できる特定のドメインだけを指定し、`*` や受け取ったオリジンの反射を使わないよう勧めています。`null` も許可しません。

</Step>
<Step title="完全一致で判定し、Vary: Origin を付ける">

前方一致や後方一致ではなく、スキームとポートを含めた文字列全体で比べます。HTTPSのサイトでHTTPのオリジンを許可しないことも大切です。

</Step>
<Step title="サーバー側の認証と認可を残す">

CORSはブラウザの動作を決めるだけです。`Origin` ヘッダーはブラウザ以外からなら自由に偽れるので、OWASPは `Origin` だけをアクセス制御の根拠にしないよう求めています。

</Step>
</Steps>

<Note tone="info">

`Access-Control-Allow-Origin: *` が問題にならないのは、誰に見られてもよい公開データを、資格情報なしで返すAPIです。OWASPは、`*` を返すURLに機密情報を含めないよう注意しています。

</Note>

## よくある質問

### Q. CORSエラーはサーバー側とブラウザ側のどちらの問題ですか？

多くはサーバーが返すヘッダーの問題です。ブラウザがレスポンスを受け取ったうえで、ヘッダーの条件に合わないとスクリプトへの受け渡しを止めています。MDNによると、セキュリティ上の理由でエラーの詳細はJavaScriptからは分からず、ブラウザの開発者ツールのコンソールにだけ表示されます。

### Q. CORSを設定すればCSRFを防げますか？

防げません。単純リクエストはプリフライトなしで送られ、サーバーで処理されます。CORSは「レスポンスを読めるか」を決めるだけなので、CSRFトークンやCookieの `SameSite` 属性などの対策が別に必要です。

### Q. 開発中だけ `*` やオリジンの反射を使ってもよいですか？

設定がそのまま本番に残る事故を避けるため、開発環境でも許可するオリジンを一覧で持つ形にしておくのが安全です。ローカル開発用のオリジン（`http://localhost:3000` など）は、開発環境の設定だけに含めます。

出典: [MDN「オリジン間リソース共有 (CORS)」](https://developer.mozilla.org/ja/docs/Web/HTTP/Guides/CORS)、[OWASP HTML5 Security Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/HTML5_Security_Cheat_Sheet.html)、[OWASP Top 10:2025 A01 Broken Access Control](https://top10.owasp.org/2025/A01_2025-Broken_Access_Control/)、[PortSwigger Web Security Academy「Cross-origin resource sharing (CORS)」](https://portswigger.net/web-security/cors)
