From d839cc4836c8c5dc3dc5d24b5cccd2c97007b84e Mon Sep 17 00:00:00 2001 From: Venkat Nikhil Date: Fri, 6 Feb 2026 15:51:56 -0700 Subject: [PATCH 1/5] fix(wrangler): validate R2 CORS file and provide helpful error messaging. Fixes #8486 --- packages/wrangler/src/r2/cors.ts | 51 +++++++++++++++++++++++--------- 1 file changed, 37 insertions(+), 14 deletions(-) diff --git a/packages/wrangler/src/r2/cors.ts b/packages/wrangler/src/r2/cors.ts index 941d61e4b2..3164a705fb 100644 --- a/packages/wrangler/src/r2/cors.ts +++ b/packages/wrangler/src/r2/cors.ts @@ -96,22 +96,45 @@ export const r2BucketCORSSetCommand = createCommand({ }, }, async handler({ bucket, file, jurisdiction, force }, { config }) { - const accountId = await requireAuth(config); + const accountId = await requireAuth(config); - const jsonFilePath = path.resolve(file); + const jsonFilePath = path.resolve(file); - const corsConfig = parseJSON(readFileSync(jsonFilePath), jsonFilePath) as { - rules: CORSRule[]; - }; + const corsConfig = parseJSON(readFileSync(jsonFilePath), jsonFilePath) as Record; - if (!corsConfig.rules || !Array.isArray(corsConfig.rules)) { - throw new UserError( - `The CORS configuration file must contain a 'rules' array as expected by the request body of the CORS API: ` + - `https://developers.cloudflare.com/api/operations/r2-put-bucket-cors-policy` - ); - } + // 1. Detect AWS S3 Top-level format (CORSRules instead of rules) + if (corsConfig.CORSRules) { + throw new UserError( + "Wrangler detected an AWS S3 CORS configuration format.\n" + + "Cloudflare R2 expects a 'rules' array instead of 'CORSRules'.\n" + + "See: https://developers.cloudflare.com/r2/buckets/cors/#example" + ); + } - if (!force) { + // 2. Validate existence of rules array + const rules = corsConfig.rules; + if (!rules || !Array.isArray(rules)) { + throw new UserError( + `The CORS configuration file must contain a 'rules' array as expected by the R2 API: ` + + `https://developers.cloudflare.com/api/operations/r2-put-bucket-cors-policy` + ); + } + + // 3. Detect AWS S3 individual rule format (AllowedOrigins, AllowedMethods, AllowedHeaders) + const hasS3Keys = (rules as Record[]).some((rule) => + "AllowedOrigins" in rule || "AllowedMethods" in rule || "AllowedHeaders" in rule + ); + + if (hasS3Keys) { + throw new UserError( + "Wrangler detected AWS S3 style keys (e.g. 'AllowedOrigins').\n" + + "Cloudflare R2 requires lowercase keys nested inside an 'allowed' object.\n" + + "Example: { \"allowed\": { \"origins\": [\"*\"], \"methods\": [\"GET\"] } }\n" + + "See: https://developers.cloudflare.com/r2/buckets/cors/#example" + ); + } + + if (!force) { const confirmedRemoval = await confirm( `Are you sure you want to overwrite the existing CORS configuration for bucket '${bucket}'?` ); @@ -122,13 +145,13 @@ export const r2BucketCORSSetCommand = createCommand({ } logger.log( - `Setting CORS configuration (${corsConfig.rules.length} rules) for bucket '${bucket}'...` + `Setting CORS configuration (${rules.length} rules) for bucket '${bucket}'...` ); await putCORSPolicy( config, accountId, bucket, - corsConfig.rules, + rules as CORSRule[], jurisdiction ); logger.log(`✨ Set CORS configuration for bucket '${bucket}'.`); From 90c3092077dc22336acc3694e2a8a7ab400fe6eb Mon Sep 17 00:00:00 2001 From: Venkat Nikhil Date: Fri, 6 Feb 2026 16:05:17 -0700 Subject: [PATCH 2/5] fix(wrangler): add R2 CORS validation and changeset --- .changeset/nasty-years-taste.md | 5 +++++ packages/wrangler/r2-test.json | 10 ++++++++++ packages/wrangler/s3-test.json | 1 + 3 files changed, 16 insertions(+) create mode 100644 .changeset/nasty-years-taste.md create mode 100644 packages/wrangler/r2-test.json create mode 100644 packages/wrangler/s3-test.json diff --git a/.changeset/nasty-years-taste.md b/.changeset/nasty-years-taste.md new file mode 100644 index 0000000000..d15c725d32 --- /dev/null +++ b/.changeset/nasty-years-taste.md @@ -0,0 +1,5 @@ +--- +"wrangler": patch +--- + +Improve validation and error messaging for R2 CORS configuration files to catch AWS S3-style formatting mistake. diff --git a/packages/wrangler/r2-test.json b/packages/wrangler/r2-test.json new file mode 100644 index 0000000000..beb50938db --- /dev/null +++ b/packages/wrangler/r2-test.json @@ -0,0 +1,10 @@ +{ + "rules": [ + { + "allowed": { + "origins": ["*"], + "methods": ["GET"] + } + } + ] +} diff --git a/packages/wrangler/s3-test.json b/packages/wrangler/s3-test.json new file mode 100644 index 0000000000..918f308662 --- /dev/null +++ b/packages/wrangler/s3-test.json @@ -0,0 +1 @@ +{"CORSRules": [{"AllowedOrigins": ["*"]}]} From 7f72952d7b20811a2df7ed837bd047589124cec4 Mon Sep 17 00:00:00 2001 From: Venkat Nikhil Date: Fri, 6 Feb 2026 16:22:56 -0700 Subject: [PATCH 3/5] fix(wrangler): add defensive check for null entries in CORS rules --- packages/wrangler/src/r2/cors.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/wrangler/src/r2/cors.ts b/packages/wrangler/src/r2/cors.ts index 3164a705fb..b467b71a5d 100644 --- a/packages/wrangler/src/r2/cors.ts +++ b/packages/wrangler/src/r2/cors.ts @@ -122,8 +122,9 @@ export const r2BucketCORSSetCommand = createCommand({ // 3. Detect AWS S3 individual rule format (AllowedOrigins, AllowedMethods, AllowedHeaders) const hasS3Keys = (rules as Record[]).some((rule) => - "AllowedOrigins" in rule || "AllowedMethods" in rule || "AllowedHeaders" in rule - ); + rule && typeof rule === "object" && !Array.isArray(rule) && + ("AllowedOrigins" in rule || "AllowedMethods" in rule || "AllowedHeaders" in rule) +); if (hasS3Keys) { throw new UserError( From c40466146f00a7b01cef9ad930f52c2f9d0968e0 Mon Sep 17 00:00:00 2001 From: Venkat Nikhil Date: Fri, 6 Feb 2026 16:39:05 -0700 Subject: [PATCH 4/5] chore: remove test JSON files from wrangler root --- packages/wrangler/r2-test.json | 10 ---------- packages/wrangler/s3-test.json | 1 - 2 files changed, 11 deletions(-) delete mode 100644 packages/wrangler/r2-test.json delete mode 100644 packages/wrangler/s3-test.json diff --git a/packages/wrangler/r2-test.json b/packages/wrangler/r2-test.json deleted file mode 100644 index beb50938db..0000000000 --- a/packages/wrangler/r2-test.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "rules": [ - { - "allowed": { - "origins": ["*"], - "methods": ["GET"] - } - } - ] -} diff --git a/packages/wrangler/s3-test.json b/packages/wrangler/s3-test.json deleted file mode 100644 index 918f308662..0000000000 --- a/packages/wrangler/s3-test.json +++ /dev/null @@ -1 +0,0 @@ -{"CORSRules": [{"AllowedOrigins": ["*"]}]} From a960018f0a3786e71e928e2fe8ec6631cca4fbb7 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Sat, 14 Mar 2026 16:56:54 +0000 Subject: [PATCH 5/5] fix: convert indentation to tabs, fix closing paren alignment, add tests for S3 format detection --- .../wrangler/src/__tests__/r2/bucket.test.ts | 42 +++++++++ packages/wrangler/src/r2/cors.ts | 88 ++++++++++--------- 2 files changed, 90 insertions(+), 40 deletions(-) diff --git a/packages/wrangler/src/__tests__/r2/bucket.test.ts b/packages/wrangler/src/__tests__/r2/bucket.test.ts index aa2651106f..34d8827264 100644 --- a/packages/wrangler/src/__tests__/r2/bucket.test.ts +++ b/packages/wrangler/src/__tests__/r2/bucket.test.ts @@ -3550,6 +3550,48 @@ describe("r2", () => { }); }); describe("set", () => { + it("should reject AWS S3 format with CORSRules key", async () => { + const filePath = "cors-s3-format.json"; + const s3Config = { + CORSRules: [ + { + AllowedOrigins: ["*"], + AllowedMethods: ["GET"], + }, + ], + }; + + fs.writeFileSync(filePath, JSON.stringify(s3Config)); + + await expect( + runWrangler( + `r2 bucket cors set my-bucket --file ${filePath} --force` + ) + ).rejects.toThrowError( + /Wrangler detected an AWS S3 CORS configuration format/ + ); + }); + + it("should reject AWS S3 style PascalCase keys in rules", async () => { + const filePath = "cors-s3-keys.json"; + const s3StyleConfig = { + rules: [ + { + AllowedOrigins: ["*"], + AllowedMethods: ["GET"], + }, + ], + }; + + fs.writeFileSync(filePath, JSON.stringify(s3StyleConfig)); + + await expect( + runWrangler( + `r2 bucket cors set my-bucket --file ${filePath} --force` + ) + ).rejects.toThrowError(/Wrangler detected AWS S3 style keys/); + }); + it("should set CORS configuration from a JSON file", async () => { const bucketName = "my-bucket"; const filePath = "cors-configuration.json"; diff --git a/packages/wrangler/src/r2/cors.ts b/packages/wrangler/src/r2/cors.ts index b467b71a5d..9dc024092a 100644 --- a/packages/wrangler/src/r2/cors.ts +++ b/packages/wrangler/src/r2/cors.ts @@ -96,46 +96,54 @@ export const r2BucketCORSSetCommand = createCommand({ }, }, async handler({ bucket, file, jurisdiction, force }, { config }) { - const accountId = await requireAuth(config); - - const jsonFilePath = path.resolve(file); - - const corsConfig = parseJSON(readFileSync(jsonFilePath), jsonFilePath) as Record; - - // 1. Detect AWS S3 Top-level format (CORSRules instead of rules) - if (corsConfig.CORSRules) { - throw new UserError( - "Wrangler detected an AWS S3 CORS configuration format.\n" + - "Cloudflare R2 expects a 'rules' array instead of 'CORSRules'.\n" + - "See: https://developers.cloudflare.com/r2/buckets/cors/#example" - ); - } - - // 2. Validate existence of rules array - const rules = corsConfig.rules; - if (!rules || !Array.isArray(rules)) { - throw new UserError( - `The CORS configuration file must contain a 'rules' array as expected by the R2 API: ` + - `https://developers.cloudflare.com/api/operations/r2-put-bucket-cors-policy` - ); - } - - // 3. Detect AWS S3 individual rule format (AllowedOrigins, AllowedMethods, AllowedHeaders) - const hasS3Keys = (rules as Record[]).some((rule) => - rule && typeof rule === "object" && !Array.isArray(rule) && - ("AllowedOrigins" in rule || "AllowedMethods" in rule || "AllowedHeaders" in rule) -); - - if (hasS3Keys) { - throw new UserError( - "Wrangler detected AWS S3 style keys (e.g. 'AllowedOrigins').\n" + - "Cloudflare R2 requires lowercase keys nested inside an 'allowed' object.\n" + - "Example: { \"allowed\": { \"origins\": [\"*\"], \"methods\": [\"GET\"] } }\n" + - "See: https://developers.cloudflare.com/r2/buckets/cors/#example" - ); - } - - if (!force) { + const accountId = await requireAuth(config); + + const jsonFilePath = path.resolve(file); + + const corsConfig = parseJSON( + readFileSync(jsonFilePath), + jsonFilePath + ) as Record; + + // Detect AWS S3 top-level format (CORSRules instead of rules) + if (corsConfig.CORSRules) { + throw new UserError( + "Wrangler detected an AWS S3 CORS configuration format.\n" + + "Cloudflare R2 expects a 'rules' array instead of 'CORSRules'.\n" + + "See: https://developers.cloudflare.com/r2/buckets/cors/#example" + ); + } + + // Validate existence of rules array + const rules = corsConfig.rules; + if (!rules || !Array.isArray(rules)) { + throw new UserError( + `The CORS configuration file must contain a 'rules' array as expected by the R2 API: ` + + `https://developers.cloudflare.com/api/operations/r2-put-bucket-cors-policy` + ); + } + + // Detect AWS S3 individual rule format (AllowedOrigins, AllowedMethods, AllowedHeaders) + const hasS3Keys = (rules as Record[]).some( + (rule) => + rule && + typeof rule === "object" && + !Array.isArray(rule) && + ("AllowedOrigins" in rule || + "AllowedMethods" in rule || + "AllowedHeaders" in rule) + ); + + if (hasS3Keys) { + throw new UserError( + "Wrangler detected AWS S3 style keys (e.g. 'AllowedOrigins').\n" + + "Cloudflare R2 requires lowercase keys nested inside an 'allowed' object.\n" + + 'Example: { "allowed": { "origins": ["*"], "methods": ["GET"] } }\n' + + "See: https://developers.cloudflare.com/r2/buckets/cors/#example" + ); + } + + if (!force) { const confirmedRemoval = await confirm( `Are you sure you want to overwrite the existing CORS configuration for bucket '${bucket}'?` );