🍔

Webブラウザのセキュリティについて理解しよう(Origin,SOP,CORS)

に公開

はじめに

はじめまして。大学4年生のとうふと申します。
今回は、3回目のZenn投稿になります!

Web開発をしていると、CORSという言葉を一度は耳にしたことがあるのではないでしょうか?
私自身初めて個人開発を始めた際、開発中に「設定しないとうまく動かないけど、そもそもCORSって何をしているの?」と疑問に思ったことがあり、Webブラウザのセキュリティについて勉強しました。

今回の記事では、そこで得た知識をできるだけコンパクトかつわかりやすくまとめ、読んでくださる皆さんにもスムーズに理解してもらえるよう工夫しています。
ぜひ最後までご覧いただけると嬉しいです!

前提

本記事では、Origin、Same-Origin Policy(SOP)、CORSについて、その経緯と必要な理由などを整理して解説していきます。
ただし、HTTPやHTTPSといった通信プロトコルの仕組みや、HTTPヘッダー全般、DOM等の詳細な説明は省略します。

これらについては、ある程度基本的な知識があることを前提に進めますので、もし不安がある場合は事前にWeb通信やWeb開発の基礎知識を軽く確認しておくと、よりスムーズに理解できるかと思います。

そもそもなぜWebブラウザでセキュリティが必要なの?

WebブラウザはユーザーがさまざまなWebサイトを自由に閲覧できる、とてもオープンな環境です。
この自由さゆえに、悪意あるサイトや人物がユーザーの個人情報を盗み見たり、勝手に操作したりするリスクが生まれます。

一例を挙げます。
URLに対してHTTPリクエストを送ると、そのリクエスト内容によってはリソースの状態を変化させる場合があります。
たとえば、あるデータに対してDELETEメソッドでリクエストを送ると、そのデータがサーバから削除されることになります。

もし悪意あるサイトがユーザーの知らない間に、ログイン中のセッション情報を使って勝手にDELETEリクエストを送ったらどうなるでしょうか?
ユーザー本人が意図していないのに、大切なデータが消えてしまうかもしれません。

このような不正な操作や情報漏洩を防ぐために、Webブラウザには制限を儲ける必要が出てきます。
次の章では基礎となるOriginについて解説します。

Originとは

Originとは、Webブラウザがリソースの出どころを判断するための情報です。
このOriginを元に、ブラウザは「どのリソース間で安全にやり取りを行うか」を決めます。
http:あるいはhttps:スキームの各URLに対するOriginは以下の三つで構成されます。

  • スキーム (例:httphttps)
  • ホスト名 (例:example.com)
  • ポート番号 (例:80443)

なお、厳密にはHTML Standardにおいて、
Documentオブジェクトなどに対するOriginの定義はさらに細かく定められており、
必ずしもこの3つだけで成り立つわけではありません。

しかし、本記事では説明を簡潔にするため、
Originを「スキーム・ホスト名・ポート番号の組み合わせ」として扱うことにします。

具体例を通してOriginを理解する

ここでは具体例としていくつかのURLを取り上げ、Originの違いをより直感的に理解できるようにします。

URL Origin(スキーム, ホスト名, ポート番号)
http://example.com:80/page1 (http, example.com, 80)
http://example.com:8080/page2 (http, example.com, 8080)
https://example.com:443/page3 (https, example.com, 443)
http://sub.example.com:80/page4 (http, sub.example.com, 80)

この表からわかるように、たとえホスト名が似ていても、

  • ポート番号
  • スキーム
  • サブドメイン

のいずれかが異なれば、ブラウザはそれらを異なるOriginとして扱います。

例えば、

  • http://example.com:80http://example.com:8080 は、ポート番号が違うため異なるOriginです。
  • http://example.com:80https://example.com:443 は、スキーム(httpとhttps)が違うため異なるOriginです。
  • http://example.com:80http://sub.example.com:80 は、ホスト名(サブドメイン部分)が違うため異なるOriginです。

このように、ブラウザはたとえ「見た目が似ているURL同士」であっても、
スキーム・ホスト名・ポート番号のどれかひとつでも違えば別のOriginと判断します。

このOriginの違いを基準にして、アクセスを許可するか制限するかを厳格に決めるのが、次に説明するSame-Origin Policyです。

Same-Origin Policy (SOP)とは

あるWebリソースにおいて、前章で説明したOriginが一致しているとき、それらはSame-Originであるといい、Originが不一致のとき、Cross-Originであるといいます。

Same-Origin Policy(SOP)とは、
このCross-Originなリソース間の
ブラウザ内アクセスを禁止する
ためのセキュリティ機構です。

SOPにより、異なるオリジンのリソース間で次のような操作が制限されます。

  • DOMの読み書き
  • CookieやlocalStorageへのアクセス
  • JavaScriptオブジェクトへの参照

この制約があることで、たとえば悪意あるサイトが他サイトの個人情報や機密データを盗み見ることを防いでいます。

なぜSOPが必要なのか?

ブラウザは、ユーザーが開いているさまざまなWebサイトを同時に扱います。
もし異なるサイト間で自由にデータアクセスが許されてしまうと、

  • ログイン中の情報を盗まれたり
  • 他サイトで勝手に操作をされてしまったり

といった深刻な被害が発生します。

こうしたリスクからユーザーを守るため、ブラウザはオリジンが違うリソース同士の自由なやりとりを禁止しているということです。

まとめると

  • Originが一致していればSame-Origin、異なればCross-Origin
  • SOPはCross-Origin間のブラウザ内アクセスを禁止するセキュリティルール
  • ユーザーの情報や操作を保護するために不可欠な仕組み

次の章ではCross-Origin Resource Sharing (CORS) について解説します。

Cross-Origin Resource Sharing (CORS)とは

前章で説明したSame-Origin Policy(SOP)は、異なるオリジン間での自由なデータアクセスを制限することで、ユーザーの安全を守っています。
しかし、これによって「安全が確認できている他サイトとの正当なデータ連携」すらも制限されてしまう場面が出てきました。

そこで登場した仕組みが、Cross-Origin Resource Sharing (CORS) です。

CORSは、サーバ側が明示的に「このオリジンからのアクセスを許可する」と宣言することで、
本来は禁止されているクロスオリジンアクセスを特例として許可できる仕組みです。

CORSの基本的な流れ

  1. フロントエンド(ブラウザ)が、別オリジン(ここではバックエンド)に対してリクエストを送信します。
    (このとき、リクエストには自動的にOriginヘッダーが付与されます。)

  2. バックエンド側のサーバは、リクエスト元のOriginを確認し、許可する場合はレスポンスヘッダーに
    Access-Control-Allow-Origin を設定して返します。

  3. ブラウザは、受け取ったレスポンスのAccess-Control-Allow-Originヘッダーを確認します。
    このヘッダーに自分自身(フロントエンド)のOriginが含まれていれば、レスポンスデータをJavaScriptから利用できるようになります。

もしサーバがAccess-Control-Allow-Originヘッダーを返さなかった場合、
たとえリクエスト自体はサーバに届き成功していたとしても、
ブラウザはセキュリティ上、レスポンスの中身をJavaScriptから参照できないようにブロックします。

認証情報が付与されたリクエストに対する制限

CORSでは、単にリクエストを許可するだけでなく、認証情報(Credential)付きのリクエストに対して特別な制限が設けられています。

ここでいう「認証情報」とは、

  • Cookie
  • Authorizationヘッダー(Bearerトークンなど)
  • TLSクライアント証明書

などを指します。


制限の内容

  1. サーバ側のレスポンスに、必ずAccess-Control-Allow-Credentials: trueを含めなければならない
  2. かつ、Access-Control-Allow-Originに具体的なオリジン(例:http://localhost:3000)を指定しなければならない

つまり、認証情報を含める場合は、

  • Access-Control-Allow-Origin: *(ワイルドカード)は使用できません。

必ずフロントエンドのオリジンを明示的に書く必要があります。


なぜこの制限があるのか?

もし認証情報付きリクエストでワイルドカード(*)を許してしまうと、どのオリジンからでも認証されたリクエストが可能になってしまい、セッション乗っ取りや情報漏洩など、深刻なセキュリティリスクが発生する可能性があります。

そのため、ブラウザは非常に厳格に

「認証情報を送るなら、サーバもリクエスト元を明示して許可しているか確認する」
という動きをします。


ブラウザ側(fetchなど)の注意点

フロントエンドでfetchを使って認証情報付きリクエストを送る場合は、
リクエスト側でも次のオプションを付ける必要があります。

fetch('http://localhost:8787/user', {
  method: 'GET',
  credentials: 'include', // 認証情報を一緒に送る
});

単純リクエストとプリフライトリクエストの違い

CORSにおけるリクエストには、2種類あります。

  • 単純リクエスト(Simple Request)
  • プリフライトリクエスト(Preflight Request)

この2つの違いを正しく理解することが、CORSを正しく設計・デバッグするために重要です。

単純リクエスト(Simple Request)とは?

単純リクエストとは、一定の安全な条件を満たしているリクエストのことを指します。
この場合、ブラウザは事前確認(プリフライト)なしでそのままリクエストをサーバに送信します。

単純リクエストの条件

  • HTTPメソッドが次のいずれかである
    • GET
    • POST
    • HEAD
  • 使われるヘッダーが限定的である
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type(かつその値がapplication/x-www-form-urlencoded, multipart/form-data, text/plainのいずれか)
  • リクエストに認証情報(Cookieなど)を含まない

これらをすべて満たす場合、リクエストは「単純リクエスト」として扱われます。

プリフライトリクエスト(Preflight Request)とは?

プリフライトリクエストとは、単純リクエストの条件を満たさないリクエストを送る前に、
ブラウザが事前にサーバへ確認リクエスト(OPTIONSメソッド)を送る仕組みです。

この事前確認によって、

  • 本当にこのリクエスト(メソッドやヘッダーなど)を受け入れていいのか?
  • サーバ側が許可しているか?

をサーバに問いかけます。

サーバが適切なCORSレスポンスを返して許可すれば、ブラウザはその後、本番のリクエスト(例えばPUTやDELETEなど)を送ります。

実際のプリフライトリクエストからの流れを見てみる

ここでは、Node.jsのHonoで起動したバックエンドと、Next.jsで実装したフロントエンドとのやりとりを例に、
プリフライトリクエストがどのように発生するのかを見ていきます。

シチュエーション

  • バックエンド(Hono)は http://localhost:8787 で動作
  • フロントエンド(Next.js)は http://localhost:3000 で動作
  • フロントエンドから、バックエンドの/admin/userエンドポイントにGETリクエストを送る

バックエンド側(Hono)の設定

バックエンドでは、CORS設定として以下を適用しています。

const app = new OpenAPIHono<AppType>();

app.use(
  "*",
  cors({
    origin: "http://localhost:3000",
    allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
    allowHeaders: ["Content-Type", "Authorization"],
    credentials: true,
  })
);

この設定により、

  • フロントエンド(http://localhost:3000)からのリクエストを許可
  • Content-TypeAuthorizationといったカスタムヘッダーも使用許可
  • 認証情報(CookieAuthorizationヘッダー)付きのリクエストも許可

となります。

プリフライトリクエストと本リクエストの流れ

ここでは、実際にフロントエンド(Next.js)からバックエンド(Hono)にリクエストを送った際の、
プリフライトリクエストと本リクエストのやり取りを簡潔にまとめます。
あるユーザデータの一覧を取得するGETメソッドを送っています。

1. プリフライトリクエスト(OPTIONS)

OPTIONS /admin/user?q=&page=1&limit=10 HTTP/1.1
Access-Control-Request-Headers: authorization
Access-Control-Request-Method: GET
Host: localhost:8787
Origin: http://localhost:3000

このようにOPTIONSメソッドのプリフライトリクエストが飛んでいることがわかります。

2. レスポンス(OPTIONS)

次にプリフライトリクエストに対するレスポンスを示します。

HTTP/1.1 204 No Content
access-control-allow-credentials: true
access-control-allow-headers: Content-Type,Authorization
access-control-allow-methods: GET,POST,PUT,DELETE,OPTIONS
access-control-allow-origin: http://localhost:3000

ここで以下のすべての要素を満たしている場合、本来送りたかったリクエストを発行します。

  • Access-Control-Allow-Originヘッダの値がリクエスト発行元のOriginを含む
  • Access-Control-Allow-Methodsヘッダの値が、元々発行した方リクエストのメソッドを含む
  • Access-Control-Allow-Headersヘッダの値が、元々発行したかったリクエストヘッダをすべて含む

次に本リクエストとそのレスポンスを示します。

3. 本リクエスト(GET)

本リクエストは以下の通りです。

GET /admin/user?q=&page=1&limit=10 HTTP/1.1
Accept: application/json, text/plain, */ *
Authorization: Bearer [実際にはトークンが入る]
Cookie: [実際にはCookie情報が入る]
Host: localhost:8787
Origin: http://localhost:3000

上記に示すように、リクエストヘッダーに

  • Authorization (認証トークン)
  • Origin (リクエスト発行元)
  • Cookie (セッション管理用)
    などが含まれることがわかります。

4. 本リクエストに対するレスポンス

最後に、本リクエストに対してサーバから返されるレスポンスです。

HTTP/1.1 200 OK
access-control-allow-credentials: true
access-control-allow-origin: http://localhost:3000
content-type: application/json

CORS制御のヘッダーが適切に設定され、フロントエンドからバックエンドのレスポンスデータを問題なく利用できることがわかります。

まとめ

ここまで、WebブラウザにおけるOrigin、Same-Origin Policy(SOP)、そしてCORSについて、
基礎から具体例まで順を追って整理してきました。

本記事のポイントをまとめると以下の通りです。

本記事のポイントのまとめ

  • Webブラウザはオープンな環境であるため、Originベースの厳しいアクセス制御(SOP) が必要
  • Originは「スキーム・ホスト名・ポート番号」の組み合わせで構成される
  • Same-Origin Policy(SOP)により、異なるオリジン間のブラウザ内アクセス(DOM、Cookieなど)が制限される
  • 安全にクロスオリジン通信を行うための仕組みがCORS(Cross-Origin Resource Sharing)
  • サーバがAccess-Control-Allow-Originなどのヘッダーで明示的に許可することで、クロスオリジン通信が可能になる
  • 特に認証情報(Credential)付きリクエストには追加の制約(credentials: true、ワイルドカード禁止)がある
  • 単純リクエストはプリフライト不要、非単純リクエストではプリフライト(OPTIONS) が自動で発生する
  • プリフライトリクエストと本リクエストの流れを理解することで、CORSエラーの原因を的確に特定できる

CORSやSOPの仕組みは、一見すると複雑に感じるかもしれませんが、
それぞれが「ユーザーとWebアプリケーションを守るため」に非常に重要な役割を担っています。

ぜひこの記事を参考に、ブラウザセキュリティの基本的な考え方を押さえて、
より安全で堅牢なWeb開発に役立ててもらえたら嬉しいです!

ここまで読んでいただき、ありがとうございました!

(追記)XSSについての記事も公開しましたのでぜひ読んでいただけたら嬉しいです!

https://zenn.dev/dem3860/articles/b427b559c98dfc

参考文献

https://hono.dev/docs/middleware/builtin/cors

https://aws.amazon.com/jp/what-is/cross-origin-resource-sharing/

Discussion