概要
Web ページに埋め込んだ JavaScript から、別のドメインにある Amazon S3 のオブジェクトを fetch で読みに行くと、URL も権限も正しいのに「読めない」ことがあります。原因は S3 ではなく、ブラウザの同一オリジンポリシーです。ブラウザは、ページと異なるオリジン(スキーム・ホスト・ポートの組)から返ってきたレスポンスを、相手が許可していない限りスクリプトに渡しません。
この制限を解くのが CORS(Cross-Origin Resource Sharing) です。S3 のバケットに CORS 設定を入れておくと、S3 はレスポンスに「このオリジンからの読み取りを許可する」というヘッダーを付けて返し、ブラウザはスクリプトにデータを渡すようになります。
ここで取り違えやすいのが、権限と CORS は別物だという点です。署名付き URL やバケットポリシーは「S3 がリクエストを受け付けるか」を決めるもので、「ブラウザがスクリプトにレスポンスを渡すか」は CORS が決めます。
この記事では、CORS の仕組みと S3 の CORS 設定の書き方、評価のされ方を整理します。実践では、CloudFront で配信したページから、別のバケットのデータを署名付き URL で読みに行きます。まず CORS 設定なしで失敗することを確かめ、CORS 設定を入れて読めるようになるまでを体験します。

クラウドおさる合鍵(署名付き URL)を持っていても、隣の山の門番(ブラウザ)が「その山の長老の許しはあるか」と止めてくることがあるのでござる。今日はその許し状、CORS 設定を書いてみるでござるよ。
この記事のメリット
- ブラウザの同一オリジンポリシーと CORS の関係を説明できるようになる
- 「権限(署名付き URL・バケットポリシー)」と「CORS」の役割の違いを区別できるようになる
- S3 の CORS 設定の JSON(
AllowedOrigins・AllowedMethodsなど)を書けるようになる - S3 が CORS 設定をどう評価するか(最初に一致したルールを使う)が分かる
- バケットをパブリックにせずに、別オリジンのページからデータを読む構成を組めるようになる
執筆者とキャラクター紹介
執筆者:土肥(とひ)
株式会社ジェニュインの代表であり元エンジニア。これまでオンプレミスと AWS 環境のインフラ業務を担当。AWS 認定資格の学習で得た知識を、ハンズオンとともに解説するのが得意。直近では新人の教育も行っており、これから技術を学ぶ方が納得できる解説を心がけています。基礎編・Cloud Practitioner の記事では、解説役としても登場します。


クラウドおさる



はじめましてでござる。弊猿はクラウドおさる。IT インフラが異常に発達した猿山から、雲に乗って地上の AWS を学びに降りてきたのでござる。



基礎編や Cloud Practitioner の記事では、とひさんに素朴な疑問をぶつける役でござるな。Security - Specialty などの記事では、要所でワンポイントや注意点をひとこと添えるでござる。



猿山にもデータセンターや監視当番があって、地上で起きることはだいたい猿山にも同じ仕組みがあるのでござる。例え話がつい猿山に寄るのは、弊猿の癖でござる。
技術解説
オリジンと同一オリジンポリシー
オリジンは「スキーム・ホスト・ポート」の組です。次の 2 つは、ホストが違うので別のオリジンです。
| 役割 | URL の例 | オリジン |
|---|---|---|
| ページ(CloudFront で配信) | https://d111111abcdef8.cloudfront.net/index.html | https://d111111abcdef8.cloudfront.net |
| データ(S3 バケット) | https://cors-lab-data-123456789012.s3.ap-northeast-1.amazonaws.com/data.json | https://cors-lab-data-123456789012.s3.ap-northeast-1.amazonaws.com |
ページの JavaScript が別オリジンへリクエストを送ると、ブラウザはリクエストに Origin ヘッダー(ページのオリジン)を付けます。レスポンスに、そのオリジンを許可する Access-Control-Allow-Origin ヘッダーが無ければ、ブラウザはレスポンスをスクリプトに渡さず、開発者ツールのコンソールに CORS エラーを出します。
ポイントは、リクエスト自体は S3 に届いていることです。止めているのはブラウザで、止めるかどうかの判断材料が S3 の返すヘッダーです。


権限と CORS は別の層
S3 にブラウザから認証済みの GET をするとき、関わる仕組みは 2 層あります。
| 層 | 決めること | 使う仕組み |
|---|---|---|
| 権限 | S3 がそのリクエストを受け付けるか | IAM ポリシー、バケットポリシー、署名付き URL |
| CORS | ブラウザがレスポンスをスクリプトに渡すか | バケットの CORS 設定 |
CORS 設定を有効にしても、ACL やポリシーは引き続き適用されます。つまり、CORS は権限を広げるものではありません。逆に、署名付き URL を渡したり、バケットポリシーでパブリックにしたりしても、CORS 設定が無ければブラウザの制限は解けません。バージョニングのような、オブジェクトの管理の設定も関係ありません。



「読めないからパブリックにする」は外れでござる。権限の層を緩めても、ブラウザの門番は CORS の許し状しか見ないのでござるぞ。しかも余計な公開まで増えてしまうでござる。
S3 の CORS 設定の書き方
S3 コンソールでは、CORS 設定を JSON で書きます。1 つの設定に最大 100 のルールを入れられます。
[
{
"AllowedHeaders": [],
"AllowedMethods": ["GET"],
"AllowedOrigins": ["https://d111111abcdef8.cloudfront.net"],
"ExposeHeaders": [],
"MaxAgeSeconds": 300
}
]| 要素 | 意味 |
|---|---|
AllowedOrigins | クロスオリジンリクエストを許可するオリジン。* で全オリジン、https://*.example.com のようにワイルドカードを 1 つだけ含められる |
AllowedMethods | 許可する HTTP メソッド。GET・PUT・POST・DELETE・HEAD から選ぶ |
AllowedHeaders | プリフライトリクエストの Access-Control-Request-Headers で許可するヘッダー |
ExposeHeaders | スクリプトから読めるようにするレスポンスヘッダー(例: x-amz-request-id) |
MaxAgeSeconds | プリフライトリクエストの結果をブラウザがキャッシュできる秒数 |
S3 が CORS 設定を評価する方法
S3 はブラウザからのリクエストを受け取ると、CORS 設定のルールを上から見て、最初に一致したルールを使います。一致するには、次の 3 つを満たす必要があります。
- リクエストの
OriginヘッダーがAllowedOriginsのいずれかと一致する - メソッド(プリフライトでは
Access-Control-Request-Method)がAllowedMethodsに含まれる - プリフライトの
Access-Control-Request-HeadersのヘッダーがAllowedHeadersと一致する
PUT や独自ヘッダー付きのリクエストでは、ブラウザが本番のリクエストの前に OPTIONS メソッドのプリフライトリクエストを送り、許可されているかを先に確かめます。ルールに一致しない場合は、403 Forbidden で「この CORS リクエストは許可されていません」というレスポンスになります。



許し状は上から順に読まれて、最初に当てはまった 1 枚だけが使われるのでござる。猿山の長老会の掟書きと同じで、細かい例外を先に、広い決まりを後に書くのがよいのでござるな。
バケットをパブリックにしない構成
今回の実践では、どちらのバケットもパブリックアクセスをすべてブロックしたままにします。
| バケット | 読み取りの手段 | パブリックにするか |
|---|---|---|
| ページ用 | CloudFront のオリジンアクセスコントロール(OAC)経由で配信 | しない(バケットポリシーで CloudFront だけ許可) |
| データ用 | 署名付き URL(作成者の s3:GetObject の権限で期間限定のアクセス) | しない |
署名付き URL は、URL の作成者が持つ権限でオブジェクトにアクセスさせる仕組みです。コンソールで作る場合の有効期限は 1 分〜12 時間、AWS CLI や SDK では最大 7 日間です。一時的な認証情報で作った URL は、指定した有効期限より前でも、その認証情報が切れた時点で無効になります。
実践
CloudFront で配信したページから、データ用バケットの data.json を署名付き URL で読みに行きます。CORS 設定なしで失敗することを確かめてから、データ用バケットに CORS 設定を入れて、読めるようになることを確かめます。
前提条件
- リージョン:
ap-northeast-1(東京) - S3・CloudFront の操作権限を持つ IAM ユーザーまたはロールでコンソールにログイン済みであること
- ブラウザは Google Chrome など、開発者ツールを開けるものを使います
以下のリソースが作成済みであることを前提とします。どちらのバケットもパブリックアクセスをすべてブロックしています。
まだ用意していない場合は、「作り方の参考」の記事の手順で作れます。リソース名は、この記事の名前に読み替えてください。
| リソース種別 | リソース名 | 内容 | 作り方の参考 |
|---|---|---|---|
| S3 バケット(データ用) | cors-lab-data-<アカウントID> | data.json を置いたバケット。CORS 設定なし | Amazon S3 でバケットを作成しオブジェクトを管理する |
| S3 バケット(ページ用) | cors-lab-page-<アカウントID> | index.html(署名付き URL を貼り付けて fetch で読むページ)を置いたバケット | 同上 |
| CloudFront ディストリビューション | (コメント cors-lab page) | ページ用バケットをオリジンアクセスコントロール cors-lab-oac 経由で配信。デフォルトルートオブジェクトは index.html | ― |
index.html は、ページ自身のオリジンを表示し、入力欄に貼り付けた URL を fetch で読みに行って、結果(成功ならレスポンスの本文、失敗ならエラー)を表示するページです。
手順 1: ページを開いてオリジンを確かめる
AWS マネジメントコンソールにログインし、上部の検索バーに CloudFront と入力して、「CloudFront」を選択します。ディストリビューションの一覧で、説明が cors-lab page のディストリビューションの「ドメイン名」をコピーします。
ブラウザの新しいタブで https://<コピーしたドメイン名>/ を開きます。「このページのオリジン」に https://dxxxxxxxxxxxxx.cloudfront.net の形のオリジンが表示されます。この値は手順 4 で使うので控えておきます。


手順 2: data.json の署名付き URL を作る
コンソールに戻り、上部の検索バーに S3 と入力して、「S3」を選択します。左側ナビゲーションの「汎用バケット」から cors-lab-data-<アカウントID> を開き、data.json のチェックボックスを選択します。
「アクション」→「署名付き URL で共有」を選択します。有効期限で「時間」を選び(デフォルトは「分」)、「時間数」に 1 を入力して、「署名付き URL を作成」をクリックします。URL は画面には表示されず、クリップボードにコピーされます。


手順 3: CORS 設定なしで読みに行く
確認ページのタブに戻り、入力欄に署名付き URL を貼り付けて「読み込む」をクリックします。結果欄に「失敗: Failed to fetch」と、ブラウザがレスポンスの読み取りをブロックしたことを示す説明が表示されます。


ブラウザの開発者ツール(Chrome では F12 キー)を開き、「Console」タブを確認すると、No 'Access-Control-Allow-Origin' header is present on the requested resource のような CORS エラーが表示されています。
同じ署名付き URL を、ブラウザのアドレスバーに直接貼り付けて開くと、data.json の中身が表示されます。URL も権限も正しく、S3 はデータを返しています。止めているのはブラウザだと分かります。



合鍵で直接開けると中身は見えるのに、ページのスクリプトからは渡してもらえないでござる。権限の層は通っていて、CORS の層で止まっているのでござるな。
手順 4: データ用バケットに CORS 設定を入れる
S3 コンソールで cors-lab-data-<アカウントID> を開き、「アクセス許可」タブを選択します。「Cross-Origin Resource Sharing (CORS)」セクションの「編集」をクリックし、以下の JSON を入力します。AllowedOrigins には手順 1 で控えたオリジンを入れます(末尾に / を付けません)。
[
{
"AllowedHeaders": [],
"AllowedMethods": ["GET"],
"AllowedOrigins": ["https://dxxxxxxxxxxxxx.cloudfront.net"],
"ExposeHeaders": [],
"MaxAgeSeconds": 300
}
]

「変更の保存」をクリックします。「アクセス許可」タブの「Cross-Origin Resource Sharing (CORS)」セクションに、入力した JSON が表示されることを確認します。
手順 5: もう一度読みに行く
確認ページのタブに戻り、同じ署名付き URL のまま「読み込む」をクリックします。結果欄に「成功: HTTP 200」と data.json の中身が表示されます。


開発者ツールの「Network」タブで data.json のリクエストを選択すると、レスポンスヘッダーに Access-Control-Allow-Origin: https://dxxxxxxxxxxxxx.cloudfront.net が付いていることを確認できます。リクエストヘッダーの Origin と一致したルールに従って、S3 がこのヘッダーを返しています。
試しに、CORS 設定の AllowedOrigins を https://example.com に書き換えて保存し、もう一度「読み込む」をクリックすると、手順 3 と同じように失敗します。Origin が一致するルールが無くなったためです。確認したら、手順 4 の値に戻しておきます。



「スクリプトから別のバケットが読めない」と言われたら、まず CORS 設定と AllowedOrigins のオリジンを疑うのでござる。署名付き URL やバケットポリシー、バージョニングをいじっても、ブラウザの門番は通してくれないでござるぞ。
まとめ
| 困りごと | 効く仕組み | 効かない仕組み |
|---|---|---|
| ページの JavaScript から別オリジンの S3 を読むと、ブラウザに止められる | バケットの CORS 設定 | 署名付き URL、バケットポリシーでの公開、バージョニング |
| S3 がリクエストを拒否する(403 AccessDenied) | IAM ポリシー、バケットポリシー、署名付き URL | CORS 設定(権限は広げない) |
- オリジンはスキーム・ホスト・ポートの組。ページとデータのオリジンが違えば、ブラウザは CORS の許可を確かめる
- S3 の CORS 設定は JSON で書き、
AllowedOrigins・AllowedMethods・AllowedHeaders・ExposeHeaders・MaxAgeSecondsで許可の範囲を決める。ルールは最大 100 個 - S3 は最初に一致したルールを使う。一致の条件は
Origin・メソッド・リクエストヘッダー PUTや独自ヘッダー付きのリクエストでは、ブラウザが先にOPTIONSのプリフライトリクエストを送る- バケットはパブリックにせず、ページは CloudFront の OAC、データは署名付き URL で読ませられる
参照先
- Cross−Origin Resource Sharing (CORS) の使用 – Amazon Simple Storage Service
- Cross−Origin Resource Sharing (CORS) の設定 – Amazon Simple Storage Service
- CORS 設定のエレメント – Amazon Simple Storage Service
- CORS のトラブルシューティング – Amazon Simple Storage Service
- 署名付き URL を使用したオブジェクトのダウンロードおよびアップロード – Amazon Simple Storage Service
- 署名付き URL を使用したオブジェクトの共有 – Amazon Simple Storage Service
- Amazon S3 オリジンへのアクセスを制限する – Amazon CloudFront















