1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【PHP】fetch が CORS で blocked される&APIトークンを JS に晒さない — サーバーサイドプロキシ構成

1
Last updated at Posted at 2026-06-27

Claude Codeで実装した時に設計漏れがあってハマったことです。
画面のブラウザ JS から PHP API を fetch したら CORS で弾かれる。
CORS を通すのは簡単ですが、セキュリティ面が不安、そこで「アクセス制限のトークンをどう持たせるか」が焦点に。
JS にトークンを書けばソースを開かれて即漏れる——。解いた構成をまとめます。

結論(先に解決策)

  • CORS: API 側(PHP)で Origin を許可リスト検証して Access-Control-Allow-Origin を返す
  • トークン秘匿: ブラウザからは API を直叩きせず、同一オリジンのサーバーサイドプロキシ(PHP)経由で呼ぶ。トークンはプロキシがサーバー内部で付与し、ブラウザには一切送らない
[ブラウザJS] --(同一オリジン/トークン無し)--> [proxy.php] --(トークン付与)--> [API index.php]

JS はトークンを知らないので、ソースを開かれても漏れません。

症状

Access to fetch at 'https://api.example.com/basic' from origin
'https://front.example.com' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.

API 側にオリジン許可ヘッダが無いだけ。これ自体は CORS ヘッダを返せば解決します。問題はその次で、「ただ乗り防止にトークンを要求したいが、トークンをどこに置くか」です。

原因と設計上のジレンマ

  • CORS で弾かれる → Access-Control-Allow-Origin が無いから
  • かといって全公開だと API にただ乗りされる → トークン認証を入れたい
  • でも JS にトークンを直書きすると、ブラウザでソースを見れば誰でも読める → 秘匿にならない

Authorization ヘッダを JS から付ける方式も、結局トークンがフロントのバンドルに乗るため同じです。**「ブラウザに渡った時点で秘密ではない」**のが根本のジレンマです。

設計段階でトークン発効に触れていればちゃんと実装してくたと思います。
トークンを発行し、サーバー経由でAPIを呼び出すように指示することによって解決に導きました。

解決

1. API 側:Origin を許可リスト検証して CORS を返す

ワイルドカード * ではなく、許可したオリジンのときだけそのオリジンを反映します。

// index.php(API本体)
$allowed = ['https://front.example.com'];
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';

if (in_array($origin, $allowed, true)) {
    header("Access-Control-Allow-Origin: $origin");
    header('Vary: Origin');
}
// プリフライト(OPTIONS)はここで 204 を返して終了
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
    http_response_code(204);
    exit;
}

2. トークンはサーバーサイドプロキシで付与する

ブラウザは自分と同じオリジンのプロキシを叩くだけ(CORS もトークンも意識しない)。プロキシがサーバー内部でトークンを足して API へ中継します。

// proxy.php(フロントと同一オリジンに置く)
$token = getenv('TEST_API_TOKEN');   // 値はサーバー側の環境変数/設定ファイルから

$ch = curl_init('https://api.example.com/' . $endpoint);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ["X-Internal-Token: $token"],  // ここでだけ付与
]);
$body = curl_exec($ch);
header('Content-Type: application/json');
echo $body;
// ブラウザ側:同一オリジンのプロキシを叩くだけ。トークンは知らない
const res = await fetch('/proxy.php?endpoint=basic');
const json = await res.json();

3. API 側でトークンと Origin/Referer を二重検証

プロキシ経由のリクエストだけ通すよう、API 本体で X-Internal-Token を検証します。Origin/Referer の許可リストと合わせて二段で守ります。

$token = $_SERVER['INTERNAL_TOKEN'] ?? '';
if (!hash_equals(getenv('TEST_API_TOKEN'), $token)) {
    http_response_code(403);
    exit(json_encode(['status' => 'error', 'message' => 'forbidden']));
}

hash_equals() を使うのは、トークン比較をタイミング攻撃に強くするためです。

なぜこの構成なのか

方式 トークンの置き場所 漏れる?
JS に直書き ブラウザのバンドル 漏れる(ソース閲覧で即)
JS から Authorization 付与 ブラウザのバンドル 漏れる(同上)
サーバーサイドプロキシで付与 サーバー側のみ 漏れない

「秘密はサーバーから外に出さない」——トークンをブラウザに渡さず、付与の責任をサーバー側プロキシに寄せるのがポイントです。

まとめ

  • CORS は API 側で Origin 許可リスト検証 → Access-Control-Allow-Origin 返却
  • トークンを JS に書くと公開ソースで漏れる
  • ブラウザは同一オリジンのプロキシを叩き、トークンはプロキシがサーバー内部で付与
  • API 本体は トークン+Origin/Referer の二重検証で守る

これは e-Stat(政府統計)の人口 API のデモ画面を作る途中で踏みました。アクセス制御の実トークン運用やデプロイ周りなど、再現可能な構成一式は別途まとめる予定です(トークンの実値はリポジトリ外で管理しています)。
API作成時はこの辺の内容を仕様に落とし込んでおく必要がありますね。

Claude Codeで開発したサイト

人口カルテ47

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?