From d9c4dd4da59b1ddf987020d6574372fc4055f493 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 27 Feb 2026 00:42:34 +0100 Subject: [PATCH 1/4] chore: add ALLOWED_ORIGINS to .env.example --- .env.example | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index 1140858..df1f07b 100644 --- a/.env.example +++ b/.env.example @@ -42,6 +42,11 @@ USDC_ISSUER_ADDRESS=GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5 # ============================================ # Frontend / API Config # ============================================ +# Allowed Origins for CORS policy (comma-separated list) +# e.g. https://app.remitwise.com, http://localhost:3000 +# Do not use * in production if credentials are required. +ALLOWED_ORIGINS=http://localhost:3000 + +# Frontend application URL (fallback if ALLOWED_ORIGINS is not set) NEXT_PUBLIC_APP_URL=http://localhost:3000 API_MAX_BODY_SIZE=1048576 - From 1e22671ea369020aa2ed2c5c2e923394429ddfd1 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 27 Feb 2026 00:42:34 +0100 Subject: [PATCH 2/4] docs: document ALLOWED_ORIGINS and security implications --- README.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 5988d99..5a42a3c 100644 --- a/README.md +++ b/README.md @@ -261,18 +261,21 @@ The middleware (`middleware.ts`) applies CORS headers, security headers, and req Cross-Origin Resource Sharing (CORS) is configured to allow requests from the frontend application: -- **Allowed Origins**: Requests from `NEXT_PUBLIC_APP_URL` are allowed (or same-origin) +- **Allowed Origins**: Configured via the `ALLOWED_ORIGINS` environment variable (comma-separated list, e.g., `https://app.remitwise.com, http://localhost:3000`). If not set, it falls back to `NEXT_PUBLIC_APP_URL`. - **Allowed Methods**: GET, POST, PUT, DELETE, PATCH, OPTIONS - **Allowed Headers**: Content-Type, Authorization, X-Requested-With -- **Credentials**: Allowed for same-origin requests +- **Credentials**: Allowed for specific origins. **Note:** Do not use `*` for origins in production if credentials are required, as browsers block credentials when `Access-Control-Allow-Origin: *` is set. If `*` is used, credentials will not be allowed. - **Preflight Handling**: OPTIONS requests return 204 No Content with appropriate CORS headers **Configuration:** -Set `NEXT_PUBLIC_APP_URL` in `.env.local`: +Set `ALLOWED_ORIGINS` in `.env.local` to specify authorized clients: ```bash -# Frontend URL for CORS policy (e.g., http://localhost:3000) +# Comma-separated list of allowed origins +ALLOWED_ORIGINS=https://app.remitwise.com,http://localhost:3000 + +# Frontend URL (fallback if ALLOWED_ORIGINS is not set) NEXT_PUBLIC_APP_URL=http://localhost:3000 ``` From acfdd53f2e1c0d505c2269d51b7da8738d869c19 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 27 Feb 2026 00:42:34 +0100 Subject: [PATCH 3/4] feat: implement CORS allow list from environment variables --- middleware.ts | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/middleware.ts b/middleware.ts index 772a75b..11792f8 100644 --- a/middleware.ts +++ b/middleware.ts @@ -28,17 +28,32 @@ const SECURITY_HEADERS: Record = { // Helper functions function applyCORS(response: NextResponse, request: NextRequest): void { - const allowedOrigin = - process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000"; + const allowedOriginsStr = + process.env.ALLOWED_ORIGINS || + process.env.NEXT_PUBLIC_APP_URL || + "http://localhost:3000"; + + const allowedOrigins = allowedOriginsStr + .split(",") + .map((o) => o.trim()) + .filter(Boolean); + const requestOrigin = request.headers.get("origin"); - const isSameOrigin = !requestOrigin || requestOrigin === allowedOrigin; - if (isSameOrigin || requestOrigin === allowedOrigin) { - response.headers.set( - "Access-Control-Allow-Origin", - requestOrigin || allowedOrigin, - ); + + if (requestOrigin) { + if ( + allowedOrigins.includes(requestOrigin) || + allowedOrigins.includes("*") + ) { + const originToSet = allowedOrigins.includes("*") ? "*" : requestOrigin; + response.headers.set("Access-Control-Allow-Origin", originToSet); + + if (originToSet !== "*") { + response.headers.set("Access-Control-Allow-Credentials", "true"); + } + } } - response.headers.set("Access-Control-Allow-Credentials", "true"); + response.headers.set( "Access-Control-Allow-Methods", CORS_ALLOWED_METHODS.join(", "), From 790af91c95e0531d6dd0a034fed6f09930a549bd Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 27 Feb 2026 00:42:34 +0100 Subject: [PATCH 4/4] test: update middleware tests for new CORS configuration --- tests/unit/middleware.test.cjs | 146 ++++++++++++++++++++++----------- 1 file changed, 100 insertions(+), 46 deletions(-) diff --git a/tests/unit/middleware.test.cjs b/tests/unit/middleware.test.cjs index 0f2a28c..cb52aa4 100644 --- a/tests/unit/middleware.test.cjs +++ b/tests/unit/middleware.test.cjs @@ -7,7 +7,7 @@ const assert = require("node:assert/strict"); */ // Mock NextRequest and NextResponse for testing -class MockNextRequest { +class MockNextRequest { constructor(method = "GET", headers = {}, body = null) { this.method = method; this.headers = new Map(Object.entries(headers)); @@ -60,26 +60,39 @@ test("applyCORS: Sets correct CORS headers for same-origin request", (t) => { // Mock applyCORS function const applyCORS = (response, request) => { - const allowedOrigin = - process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000"; + const allowedOriginsStr = + process.env.ALLOWED_ORIGINS || + process.env.NEXT_PUBLIC_APP_URL || + "http://localhost:3000"; + + const allowedOrigins = allowedOriginsStr + .split(",") + .map((o) => o.trim()) + .filter(Boolean); + const requestOrigin = request.headers.get("origin"); - const isSameOrigin = !requestOrigin || requestOrigin === allowedOrigin; - if (isSameOrigin || requestOrigin === allowedOrigin) { - response.set( - "Access-Control-Allow-Origin", - requestOrigin || allowedOrigin, - ); + if (requestOrigin) { + if ( + allowedOrigins.includes(requestOrigin) || + allowedOrigins.includes("*") + ) { + const originToSet = allowedOrigins.includes("*") ? "*" : requestOrigin; + response.set("Access-Control-Allow-Origin", originToSet); + + if (originToSet !== "*") { + response.set("Access-Control-Allow-Credentials", "true"); + } + } } - response.set("Access-Control-Allow-Credentials", "true"); response.set( "Access-Control-Allow-Methods", - "GET, POST, PUT, DELETE, PATCH, OPTIONS", + "GET, POST, PUT, DELETE, PATCH, OPTIONS" ); response.set( "Access-Control-Allow-Headers", - "Content-Type, Authorization, X-Requested-With", + "Content-Type, Authorization, X-Requested-With" ); response.set("Vary", "Origin"); }; @@ -113,26 +126,39 @@ test("applyCORS: Sets CORS headers for request from allowed origin", (t) => { const response = MockNextResponse.next(); const applyCORS = (response, request) => { - const allowedOrigin = - process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000"; + const allowedOriginsStr = + process.env.ALLOWED_ORIGINS || + process.env.NEXT_PUBLIC_APP_URL || + "http://localhost:3000"; + + const allowedOrigins = allowedOriginsStr + .split(",") + .map((o) => o.trim()) + .filter(Boolean); + const requestOrigin = request.headers.get("origin"); - const isSameOrigin = !requestOrigin || requestOrigin === allowedOrigin; - if (isSameOrigin || requestOrigin === allowedOrigin) { - response.set( - "Access-Control-Allow-Origin", - requestOrigin || allowedOrigin, - ); + if (requestOrigin) { + if ( + allowedOrigins.includes(requestOrigin) || + allowedOrigins.includes("*") + ) { + const originToSet = allowedOrigins.includes("*") ? "*" : requestOrigin; + response.set("Access-Control-Allow-Origin", originToSet); + + if (originToSet !== "*") { + response.set("Access-Control-Allow-Credentials", "true"); + } + } } - response.set("Access-Control-Allow-Credentials", "true"); response.set( "Access-Control-Allow-Methods", - "GET, POST, PUT, DELETE, PATCH, OPTIONS", + "GET, POST, PUT, DELETE, PATCH, OPTIONS" ); response.set( "Access-Control-Allow-Headers", - "Content-Type, Authorization, X-Requested-With", + "Content-Type, Authorization, X-Requested-With" ); response.set("Vary", "Origin"); }; @@ -152,31 +178,46 @@ test("applyCORS: Falls back to NEXT_PUBLIC_APP_URL when env var set", (t) => { const originalEnv = process.env.NEXT_PUBLIC_APP_URL; process.env.NEXT_PUBLIC_APP_URL = "http://localhost:3000"; - const request = new MockNextRequest("GET", {}); + const request = new MockNextRequest("GET", { + origin: "http://localhost:3000" + }); const response = MockNextResponse.next(); const applyCORS = (response, request) => { - const allowedOrigin = - process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000"; + const allowedOriginsStr = + process.env.ALLOWED_ORIGINS || + process.env.NEXT_PUBLIC_APP_URL || + "http://localhost:3000"; + + const allowedOrigins = allowedOriginsStr + .split(",") + .map((o) => o.trim()) + .filter(Boolean); + const requestOrigin = request.headers.get("origin"); - const isSameOrigin = !requestOrigin || requestOrigin === allowedOrigin; - if (isSameOrigin || requestOrigin === allowedOrigin) { - response.set( - "Access-Control-Allow-Origin", - requestOrigin || allowedOrigin, - ); + if (requestOrigin) { + if ( + allowedOrigins.includes(requestOrigin) || + allowedOrigins.includes("*") + ) { + const originToSet = allowedOrigins.includes("*") ? "*" : requestOrigin; + response.set("Access-Control-Allow-Origin", originToSet); + + if (originToSet !== "*") { + response.set("Access-Control-Allow-Credentials", "true"); + } + } } - response.set("Access-Control-Allow-Credentials", "true"); response.set( "Access-Control-Allow-Methods", - "GET, POST, PUT, DELETE, PATCH, OPTIONS", + "GET, POST, PUT, DELETE, PATCH, OPTIONS" ); response.set( "Access-Control-Allow-Headers", - "Content-Type, Authorization, X-Requested-With", + "Content-Type, Authorization, X-Requested-With" ); response.set("Vary", "Origin"); }; @@ -460,26 +501,39 @@ test("Middleware: CORS applied before rate limiting", (t) => { // CORS should be applied first const applyCORS = (response, request) => { - const allowedOrigin = - process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000"; + const allowedOriginsStr = + process.env.ALLOWED_ORIGINS || + process.env.NEXT_PUBLIC_APP_URL || + "http://localhost:3000"; + + const allowedOrigins = allowedOriginsStr + .split(",") + .map((o) => o.trim()) + .filter(Boolean); + const requestOrigin = request.headers.get("origin"); - const isSameOrigin = !requestOrigin || requestOrigin === allowedOrigin; - if (isSameOrigin || requestOrigin === allowedOrigin) { - response.set( - "Access-Control-Allow-Origin", - requestOrigin || allowedOrigin, - ); + if (requestOrigin) { + if ( + allowedOrigins.includes(requestOrigin) || + allowedOrigins.includes("*") + ) { + const originToSet = allowedOrigins.includes("*") ? "*" : requestOrigin; + response.set("Access-Control-Allow-Origin", originToSet); + + if (originToSet !== "*") { + response.set("Access-Control-Allow-Credentials", "true"); + } + } } - response.set("Access-Control-Allow-Credentials", "true"); response.set( "Access-Control-Allow-Methods", - "GET, POST, PUT, DELETE, PATCH, OPTIONS", + "GET, POST, PUT, DELETE, PATCH, OPTIONS" ); response.set( "Access-Control-Allow-Headers", - "Content-Type, Authorization, X-Requested-With", + "Content-Type, Authorization, X-Requested-With" ); response.set("Vary", "Origin"); };