ブラウザーからのアップロード元を設定する

CloudflareBeginner
オンラインで実践に進む

はじめに

presigned URL が curl では動作しても、ブラウザーからのアップロードページは失敗することがあります。ブラウザーは、ストレージサービスがそのページのオリジンを許可しているかどうかも確認するためです。この実験では、バケットを非公開のまま、用意された合成アップロードページに対して独立した CORS の境界を設定します。

最初に「一時ファイルアクセスを承認する」実験を完了してください。この新しい VM には、固定バージョンの Node.js 22.22.0、Wrangler 4.131.1、AWS SDK 3.888.0、および今回とは無関係なアップロードページのシェルが用意されています。新しい非公開バケット、短期間有効なオブジェクト認証情報、使い捨ての Worker ページを作成します。R2 はあらかじめ有効化されている必要があります。R2 の料金CORS の動作を確認してください。購入済みのドメインは必要ありません。用意された合成テキストだけを使用し、すべてのリソースを削除して、実験用トークンを失効させてください。

アプリケーション用バケットに接続する

このステップでは、この VM を認証し、アプリケーション用の独立した非公開バケットを作成します。デバイス認証によって学習用アカウントを確認します。R2 バケットの管理には、そのアカウントだけに制限した別の API トークンを使用します。

以下で使用するコマンド構文のために Bash を起動し、準備済みのプロジェクトへ移動してツールを確認します。リソース名の変数を利用できるよう、同じターミナルを開いたままにしてください。

bash
cd /home/labex/project/r2-lab
export PATH="$PWD/.tools/node-v22.22.0-linux-x64/bin:$PATH"
node --version
npx wrangler --version

表示されたデバイスコードを、自分のブラウザーで承認します。同意する前に、学習用アカウントと、要求されているアカウントおよびユーザーの読み取りスコープを確認してください。

npx wrangler login --device --browser=false --scopes account:read user:read workers_scripts:write workers_kv:write
npx wrangler whoami --json

loggedIn: true になっていることを確認します。アカウントが 1 つだけ表示される場合でも、アカウント名を読み取ってください。以下の YOUR_ACCOUNT_ID を、そのアカウントの実際の 32 文字の ID に置き換えます。openssl rand -hex 6 は 12 個のランダムな 16 進数文字を生成するため、この実験で以前の実行と名前が衝突することはありません。ヒアドキュメントによって標準の設定ファイルが作成され、シェルが変数をその中に展開します。

ACCOUNT_ID=YOUR_ACCOUNT_ID
RUN_ID=$(openssl rand -hex 6)
NAME="labex-c05-r05-$RUN_ID"
BUCKET="$NAME-docs"
cat > wrangler.jsonc <<JSON
{"name":"$NAME","account_id":"$ACCOUNT_ID","main":"src/index.js","workers_dev":true,"compatibility_date":"2026-07-30","r2_buckets":[{"binding":"DOCUMENTS","bucket_name":"$BUCKET"}]}
JSON

バケットを管理するには、Cloudflare プロフィールの API Tokens ページを開き、この実験にちなんだ名前でカスタムトークンを作成します。Account → Workers R2 Storage → Edit を許可し、Account Resources を、保存した ID の学習用アカウントだけに制限します。有効期限は短く設定してください。他のアカウントや関係のない権限は含めないでください。このアカウントレベルの権限でバケットを作成・削除します。次のステップで作成するオブジェクト専用トークンでは、これらの操作はできません。

トークンを一度だけコピーし、この非表示の VM プロンプトに入力します。umask 077 によってファイルを自分のユーザーだけが読み取れるようにし、read -s によって入力を非表示にします。このファイルでは Wrangler の標準トークン変数を使用し、Git から除外されます。

umask 077
read -r -s -p 'R2 management API token: ' R2_MANAGEMENT_TOKEN; printf '\n'
printf 'CLOUDFLARE_API_TOKEN=%s\n' "$R2_MANAGEMENT_TOKEN" > .env.management
unset R2_MANAGEMENT_TOKEN

R2 管理コマンドにだけ --env-file=.env.management を使用してください。通常の whoami は、引き続き VM のデバイス認証を確認します。

--env-file は各 Wrangler コマンドの末尾に置きます。これにより、ファイル引数のリストにコマンド名が取り込まれるのを防げます。各バケットの作成後、Wrangler が設定へのバインディング追加を尋ねたら、n を入力して Enter を押してください。必要なバインディングはすでに設定されています。

npx wrangler r2 bucket create "$BUCKET" --env-file=.env.management

バケット一覧を表示し、生成された正確な名前を探します。他のバケットは別の作業に使われているため、そのままにしてください。

npx wrangler r2 bucket list --env-file=.env.management

Dashboard で Storage & databases → R2 → Overview を開き、この正確なバケットを選択して、オブジェクト一覧が空であることを確認します。バケットの設定では、パブリック開発 URL とカスタムドメインを無効のままにしてください。Dashboard に表示されるバケット名で対象を確認できます。保存されたバイト列は、後のダウンロード確認で検証します。

Worker スクリプトの権限はデプロイに必要です。KV 権限は Wrangler の削除処理における記録管理を支援しますが、この実験では KV namespace を作成しません。R2 管理トークンは、アカウントに制限された別の認証情報として保持されます。

アップロードページと署名用アクセスを準備する

このステップでは、用意されたページをデプロイし、ターミナルにこのバケット専用の認証情報を設定します。ページには固定された合成ペイロードと空の URL フィールドだけが含まれ、長期間有効な署名用シークレットを受け取ることはありません。

S3 互換 API によって、標準のストレージ SDK から R2 にアクセスできます。Wrangler のデバイストークンとは別に、アクセスキーのペアを使用します。R2 Overview で Account Details → API Tokens → Manage を開き、この実験で生成したリソース名にちなんだ名前で User API token を作成します。Object Read & Write を選択し、この新しいバケットだけに制限します。フォームに項目がある場合は、短い有効期限を選択してください。すべてのバケットや Admin アクセスは選択しないでください。一度だけ表示されるシークレットを保存するまで、このトークンのページを開いたままにしてください。

VM で次の Bash プロンプトを使用します。read -s によって入力を非表示にし、umask 077 によって認証情報ファイルを自分のユーザーだけが読み取れるようにします。これらは標準の AWS SDK 環境変数名です。Access Key ID と Secret Access Key をそれぞれのプロンプトに貼り付け、Enter キーを押してください。一般的な API トークンの値は貼り付けないでください。

umask 077
read -r -s -p 'Access Key ID: ' AWS_ACCESS_KEY_ID; printf '\n'
read -r -s -p 'Secret Access Key: ' AWS_SECRET_ACCESS_KEY; printf '\n'
printf 'AWS_ACCESS_KEY_ID=%s\nAWS_SECRET_ACCESS_KEY=%s\n' "$AWS_ACCESS_KEY_ID" "$AWS_SECRET_ACCESS_KEY" > .env.s3
unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY

再利用できる標準 SDK クライアントを作成します。SDK にはリージョン文字列が必要で、R2 では auto を使用します。既存の設定を読み込むことで、CLI と SDK の操作対象を同じアカウントとバケットにそろえます。

cat > storage.mjs <<'JS'
import { S3Client } from "@aws-sdk/client-s3";
import { readFileSync } from "node:fs";
const config = JSON.parse(readFileSync("wrangler.jsonc", "utf8"));
export const Bucket = config.r2_buckets[0].bucket_name;
export const s3 = new S3Client({
  region: "auto",
  requestChecksumCalculation: "WHEN_REQUIRED",
  endpoint: `https://${config.account_id}.r2.cloudflarestorage.com`,
  credentials: {
    accessKeyId: process.env.AWS_ACCESS_KEY_ID,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
  }
});
JS

requestChecksumCalculation: "WHEN_REQUIRED" は、ブラウザーが本文を送る前に空の本文のチェックサムを署名することを防ぎます。URL は引き続きオブジェクトと操作を署名し、以下の署名呼び出しでは Content-Type を明示的に含めます。最後の検証で実際に保存されたバイトを比較します。

用意されたページを公開します。

npx wrangler deploy

HTTPS のデプロイ URL を、パスや末尾のスラッシュを付けずに ORIGIN へ設定します。origin はスキーム、ホスト名、ポートで構成されます。https://example.comhttp://example.com は異なるオリジンです。後で独立した確認に使うため、保存してください。

ORIGIN=https://YOUR_WORKER.YOUR_SUBDOMAIN.workers.dev
printf "%s\n" "$ORIGIN" > origin.txt

ブラウザーでその URL を開きます。Synthetic export upload ページが表示され、temporary URL フィールドが空になっていることを確認します。まだアップロードしないでください。この時点では、バケットがこのブラウザーのオリジンをまだ許可していません。

1 つのブラウザーオリジンを許可する

このステップでは、Cross-Origin Resource Sharing(CORS) を設定します。ブラウザーは、クロスオリジンの PUT を送信する前に、このページのオリジン、メソッド、ヘッダーが許可されているかをバケットへ確認します。この OPTIONS リクエストをプリフライトと呼びます。CORS はブラウザーからのアクセスを制御するものであり、署名された操作に対するストレージ認証の代わりにはなりません。

実際のページのオリジンを使ってポリシーを作成します。ターミナルが $ORIGIN を JSON に展開します。Wrangler では、AWS の CORSRules 形式ではなく、Cloudflare の小文字の rulesallowed 形式を使用します。

cat > cors.json <<JSON
{"rules":[{"allowed":{"origins":["$ORIGIN"],"methods":["PUT"],"headers":["content-type"]},"exposeHeaders":["ETag"],"maxAgeSeconds":60}]}
JSON

用意されたページで必要なのは PUT と Content-Type だけです。exposeHeaders によって JavaScript が返された ETag を読み取れるようになり、短い maxAgeSeconds によってテスト中のプリフライトのキャッシュ期間を短くできます。

npx wrangler r2 bucket cors set "$BUCKET" --file cors.json --env-file=.env.management
npx wrangler r2 bucket cors list "$BUCKET" --env-file=.env.management

この新しく作成したバケットだけにポリシーを上書きしたことを確認します。結果の一覧に、指定した正確なオリジンが表示されている必要があります。バケットの Dashboard 設定で CORS ポリシーを読み取り専用で確認し、パブリックアクセスは無効のままにしてください。

アップロードページの CORS ポリシー

例:このポリシーでは、このページのオリジン、PUT、content-type を許可しています。生成されるホスト名は異なります。CLI 出力では ETag の公開とキャッシュ時間も確認します。

ブラウザーからアップロードする

このステップでは、署名付き PUT の権限とブラウザーのオリジンポリシーを組み合わせます。前の実験で学んだ署名処理によって、正確に uploads/browser.txt 用の URL を作成します。ページは、署名時に指定したものと同じ Content-Type を送信します。

cat > sign-upload.mjs <<'JS'
import { PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { writeFileSync } from "node:fs";
import { s3, Bucket } from "./storage.mjs";
const url = await getSignedUrl(s3, new PutObjectCommand({
  Bucket, Key: "uploads/browser.txt", ContentType: "text/plain"
}), { expiresIn: 300, signableHeaders: new Set(["content-type"]) });
writeFileSync("put-url.txt", url, { mode: 0o600 });
console.log("Prepared a five-minute PUT URL");
JS
node --env-file=.env.s3 sign-upload.mjs

この一時リンクは、用意されたページへコピーする間だけ表示してください。共有したり、スクリーンショットに含めたり、代わりに S3 シークレットを貼り付けたりしないでください。

cat put-url.txt

リンクを Temporary PUT URL に貼り付け、5 分以内に Upload synthetic text を選択します。2xx の HTTP 成功結果と、表示された ETag を確認してください。アップロード後に URL フィールドを空にします。期限切れの URL は、エラーレスポンスに CORS ヘッダーが含まれないため、一般的なブラウザーの CORS エラーとして表示されることがあります。必要に応じて、新しいリンクを生成してください。

Wrangler で実際のオブジェクトをダウンロードし、固定ペイロードと比較します。

npx wrangler r2 object get "$BUCKET/uploads/browser.txt" --remote --file browser-download.txt --env-file=.env.management
printf "Synthetic browser export.\n" > expected-browser.txt
cmp expected-browser.txt browser-download.txt

プラットフォームのチェックでは、バイト列と Content-Type を独立して読み取ります。この実験では、実際にブラウザーで操作したことが必要な証拠です。curl だけではブラウザーの CORS 制約は適用されません。

ブラウザーでのアップロード成功

実際のブラウザーが HTTP 200 と読み取り可能な ETag を返し、一時 URL 欄は消去済みです。ページには \n が文字として表示されますが、実際には改行をアップロードします。独立したダウンロード検証で正確な 26 バイトの内容を確認します。

オリジンポリシーと署名権限を分けて確認する

このステップでは、別のオブジェクトを作成せずに、許可されたプリフライトと拒否されたプリフライトを確認します。次の curl リクエストは、ブラウザーに許可されている操作を問い合わせるものです。curl 自体は、その応答内容を強制しません。

ACCOUNT_ID=$(node -p "JSON.parse(require('fs').readFileSync('wrangler.jsonc')).account_id")
OBJECT_URL="https://$ACCOUNT_ID.r2.cloudflarestorage.com/$BUCKET/uploads/browser.txt"
curl -i -X OPTIONS -H "Origin: $ORIGIN" -H "Access-Control-Request-Method: PUT" -H "Access-Control-Request-Headers: content-type" "$OBJECT_URL"

Access-Control-Allow-Origin が、ページの正確なオリジンと一致していることを確認します。次に、関係のないオリジンから問い合わせます。

curl -i -X OPTIONS -H "Origin: https://outside.example" -H "Access-Control-Request-Method: PUT" -H "Access-Control-Request-Headers: content-type" "$OBJECT_URL"

この応答では、そのオリジン(または *)を許可してはいけません。拒否応答の正確なステータスは異なる場合があります。ブラウザーからのアクセスを防ぐのは、許可が返されないことです。最後に、署名なしで読み取りを試みます。

curl -sS -o unsigned.xml -w "%{http_code}\n" "$OBJECT_URL"

この R2 S3 エンドポイントへの完全に署名のないリクエストでは、400 と XML Code InvalidArgument、Message Authorization を確認します。以下の両フィールドを調べてください。任意のエラーやネットワーク障害は署名の拒否を証明しません。

python3 - <<'PYXML'
from xml.etree import ElementTree
root = ElementTree.parse("unsigned.xml").getroot()
print("Code:", root.findtext("Code"))
print("Message:", root.findtext("Message"))
PYXML

CORS を有効にしても、バケットが公開されたり、署名チェックがなくなったりすることはありません。有効な署名を持つ非ブラウザークライアントは、Origin ヘッダーの有無に関係なく、許可された操作を実行できます。オリジン文字列は本人確認用の認証情報ではありません。

アップロード用リソースと認証情報を削除する

このステップでは、UI Worker、対象のオブジェクト、バケットを削除します。認証情報を失効させる前に、リソースが存在しないことを確認してください。

npx wrangler delete
npx wrangler r2 object delete "$BUCKET/uploads/browser.txt" --remote --env-file=.env.management
npx wrangler r2 bucket delete "$BUCKET" --env-file=.env.management
npx wrangler r2 bucket list --env-file=.env.management

この実験で生成した名前のリソースだけが対象になっていることを確認します。Dashboard の Worker 一覧とバケット一覧を更新してください。管理認証が有効なうちに、プラットフォームのクリーンアップチェックを実行します。バケットを削除すると、そのバケットの CORS 設定も削除されます。

実験用認証情報を失効させてログアウトする

このステップでは、この実験によって残ったアクセスを閉じます。R2 API Tokens ページで、この実験用の名前が付いたオブジェクトトークンだけを失効させます。プロフィールの API Tokens ページで、この実験用に作成した別の R2 管理トークンを失効させます。バケットを削除してもトークンは失効せず、Wrangler からログアウトしても S3 認証情報は失効しません。

失効後、ローカルの認証情報ファイルを削除し、この VM からログアウトします。

rm .env.s3 .env.management put-url.txt
npx wrangler logout

構造化された ID 情報を確認します。ログアウト後にステータスが 0 以外になるのは想定どおりです。

npx wrangler whoami --json || true

loggedIn: false になっていることを確認します。通常の Dashboard ログインは維持してください。プラットフォームは、ローカル認証情報の削除と Wrangler のログアウトを確認します。この実験候補では、2 つのトークンの失効は Dashboard で手動確認するチェックポイントであり、ファイル削除から自動的に判断されるものではありません。

まとめ

完全一致のオリジンに対する CORS を設定し、presigned URL で合成バイト列をアップロードし、ブラウザーの許可と認証を区別して、リソースをクリーンアップしました。