From 3ae116310225003d61a251080c4cc2c68da669bf Mon Sep 17 00:00:00 2001 From: Mark McIntosh Date: Fri, 20 Feb 2026 08:56:22 -0500 Subject: [PATCH 1/2] security: restrict CORS to explicit allowed origins - Replace wildcard origin:'*' with dynamic CORS_ORIGINS check - No CORS_ORIGINS env var = reject all cross-origin requests (secure default) - Add CORS_ORIGINS to Bindings interface - Add X-API-Key to allowed headers - Add CORS_ORIGINS=http://localhost:8787 to dev wrangler.toml configs - Same-origin requests (admin UI) are unaffected Breaking: cross-origin API consumers must be listed in CORS_ORIGINS Fixes VULN-003 --- my-sonicjs-app/wrangler.toml | 1 + packages/core/src/app.ts | 1 + packages/core/src/routes/api.ts | 9 +++++++-- packages/create-app/templates/starter/wrangler.toml | 1 + 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/my-sonicjs-app/wrangler.toml b/my-sonicjs-app/wrangler.toml index bd6623f50..9f5eb3650 100644 --- a/my-sonicjs-app/wrangler.toml +++ b/my-sonicjs-app/wrangler.toml @@ -30,6 +30,7 @@ id = "a16f8246fc294d809c90b0fb2df6d363" # Environment variables [vars] ENVIRONMENT = "development" +CORS_ORIGINS = "http://localhost:8787" # Observability [observability] diff --git a/packages/core/src/app.ts b/packages/core/src/app.ts index 3865b457c..5e53faf6f 100644 --- a/packages/core/src/app.ts +++ b/packages/core/src/app.ts @@ -53,6 +53,7 @@ export interface Bindings { IMAGES_ACCOUNT_ID?: string IMAGES_API_TOKEN?: string ENVIRONMENT?: string + CORS_ORIGINS?: string BUCKET_NAME?: string GOOGLE_MAPS_API_KEY?: string } diff --git a/packages/core/src/routes/api.ts b/packages/core/src/routes/api.ts index bcac2ff86..efc52a0e1 100644 --- a/packages/core/src/routes/api.ts +++ b/packages/core/src/routes/api.ts @@ -33,9 +33,14 @@ apiRoutes.use('*', async (c, next) => { // Add CORS middleware apiRoutes.use('*', cors({ - origin: '*', + origin: (origin, c) => { + const allowed = (c.env as any)?.CORS_ORIGINS as string | undefined + if (!allowed) return null // No env var = reject cross-origin (secure default) + const list = allowed.split(',').map((s: string) => s.trim()) + return list.includes(origin) ? origin : null + }, allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], - allowHeaders: ['Content-Type', 'Authorization'] + allowHeaders: ['Content-Type', 'Authorization', 'X-API-Key'] })) // Helper function to add timing metadata diff --git a/packages/create-app/templates/starter/wrangler.toml b/packages/create-app/templates/starter/wrangler.toml index fcc7ea552..2667a3f8c 100644 --- a/packages/create-app/templates/starter/wrangler.toml +++ b/packages/create-app/templates/starter/wrangler.toml @@ -21,6 +21,7 @@ bucket_name = "my-sonicjs-media" # Environment variables [vars] ENVIRONMENT = "development" +CORS_ORIGINS = "http://localhost:8787" # Production environment [env.production] From cd6828d908d756504dbb0db655e904b8a602e013 Mon Sep 17 00:00:00 2001 From: Mark McIntosh Date: Fri, 20 Feb 2026 09:42:05 -0500 Subject: [PATCH 2/2] fix: update CORS E2E tests to use configured origin Tests were sending Origin headers (localhost:3000, example.com) that don't match the CORS_ORIGINS allowlist. Updated to use http://localhost:8787 and assert the echoed origin instead of wildcard '*'. --- tests/e2e/07-api.spec.ts | 11 ++++++----- tests/e2e/08-collections-api.spec.ts | 8 +++++--- tests/e2e/smoke.spec.ts | 8 ++++---- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/tests/e2e/07-api.spec.ts b/tests/e2e/07-api.spec.ts index 6c939faee..cf0ccc52e 100644 --- a/tests/e2e/07-api.spec.ts +++ b/tests/e2e/07-api.spec.ts @@ -55,17 +55,18 @@ test.describe('API Endpoints', () => { }); test('should handle CORS for API endpoints', async ({ request }) => { + // Use the origin configured in CORS_ORIGINS (wrangler.toml) const response = await request.get('/api', { headers: { - 'Origin': 'http://localhost:3000' + 'Origin': 'http://localhost:8787' } }); - + expect(response.ok()).toBeTruthy(); - - // Check for CORS headers + + // Check for CORS headers — should echo back the allowed origin const corsHeader = response.headers()['access-control-allow-origin']; - expect(corsHeader).toBeDefined(); + expect(corsHeader).toBe('http://localhost:8787'); }); test('should handle content negotiation', async ({ request }) => { diff --git a/tests/e2e/08-collections-api.spec.ts b/tests/e2e/08-collections-api.spec.ts index 1b557fc35..5f1a10c85 100644 --- a/tests/e2e/08-collections-api.spec.ts +++ b/tests/e2e/08-collections-api.spec.ts @@ -78,14 +78,16 @@ test.describe('Collections API', () => { }); test('should handle CORS headers', async ({ request }) => { + // Use the origin configured in CORS_ORIGINS (wrangler.toml) const response = await request.get('/api/collections', { headers: { - 'Origin': 'https://example.com' + 'Origin': 'http://localhost:8787' } }); - + expect(response.ok()).toBeTruthy(); - expect(response.headers()['access-control-allow-origin']).toBe('*'); + // CORS now echoes back the allowed origin instead of wildcard + expect(response.headers()['access-control-allow-origin']).toBe('http://localhost:8787'); }); test('should have consistent timestamp format', async ({ request }) => { diff --git a/tests/e2e/smoke.spec.ts b/tests/e2e/smoke.spec.ts index 43aac6136..134495da4 100644 --- a/tests/e2e/smoke.spec.ts +++ b/tests/e2e/smoke.spec.ts @@ -248,18 +248,18 @@ test.describe('Smoke Tests - Critical Path', () => { }); test('CORS headers are present on API endpoints', async ({ request }) => { + // Use the origin configured in CORS_ORIGINS (wrangler.toml) const response = await request.get('/api', { headers: { - 'Origin': 'http://localhost:3000' + 'Origin': 'http://localhost:8787' } }); expect(response.ok()).toBeTruthy(); - // Verify CORS header is present + // Verify CORS header echoes back the allowed origin const corsHeader = response.headers()['access-control-allow-origin']; - expect(corsHeader).toBeDefined(); - expect(corsHeader).toBeTruthy(); + expect(corsHeader).toBe('http://localhost:8787'); }); test('API returns correct content-type headers', async ({ request }) => {