From 1107a233aeb96fe6840c2fdfe254786e24310f30 Mon Sep 17 00:00:00 2001 From: Flavien David Date: Thu, 20 Mar 2025 21:46:13 +0100 Subject: [PATCH 1/6] Add support for CORS on our public API --- front/config/cors.ts | 8 +++++++ front/middleware.ts | 55 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 front/config/cors.ts diff --git a/front/config/cors.ts b/front/config/cors.ts new file mode 100644 index 000000000000..c5ebeda7fbbd --- /dev/null +++ b/front/config/cors.ts @@ -0,0 +1,8 @@ +const ALLOWED_ORIGINS = ["https://front-ext.dust.tt"] as const; +type AllowedOriginType = (typeof ALLOWED_ORIGINS)[number]; + +export function isAllowedOrigin(origin: string): origin is AllowedOriginType { + return ALLOWED_ORIGINS.includes(origin as AllowedOriginType); +} + +export const DEV_ORIGIN = "http://localhost:3010" as const; diff --git a/front/middleware.ts b/front/middleware.ts index 3a2a42d2b630..785592a60451 100644 --- a/front/middleware.ts +++ b/front/middleware.ts @@ -1,6 +1,8 @@ import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; +import { DEV_ORIGIN, isAllowedOrigin } from "@app/config/cors"; + export function middleware(request: NextRequest) { const url = request.nextUrl.pathname; @@ -49,9 +51,62 @@ export function middleware(request: NextRequest) { }); } + // Handle CORS for public API endpoints + if (url.startsWith("/v1")) { + if (request.method === "OPTIONS") { + const response = new NextResponse(null, { status: 200 }); + setCorsHeaders(response, request); + return response; + } + + const response = NextResponse.next(); + setCorsHeaders(response, request); + return response; + } + + // Handle development environment CORS + if (process.env.NODE_ENV === "development") { + if (request.method === "OPTIONS") { + const response = new NextResponse(null, { status: 200 }); + setDevCorsHeaders(response); + return response; + } + + const response = NextResponse.next(); + setDevCorsHeaders(response); + return response; + } + return NextResponse.next(); } +function setCorsHeaders(response: NextResponse, request: NextRequest) { + const origin = request.headers.get("origin"); + if (origin && isAllowedOrigin(origin)) { + response.headers.set("Access-Control-Allow-Origin", origin); + response.headers.set("Access-Control-Allow-Credentials", "true"); + } + + response.headers.set( + "Access-Control-Allow-Methods", + "GET, POST, PUT, DELETE, OPTIONS" + ); + response.headers.set( + "Access-Control-Allow-Headers", + "Authorization, Content-Type, X-Request-Origin, x-Commit-Hash, X-Dust-Extension-Version" + ); +} + +function setDevCorsHeaders(response: NextResponse) { + response.headers.set("Access-Control-Allow-Origin", DEV_ORIGIN); + response.headers.set("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); + response.headers.set( + "Access-Control-Allow-Headers", + "Authorization, X-Request-Origin, x-Commit-Hash, X-Dust-Extension-Version, Content-Type" + ); + response.headers.set("Access-Control-Allow-Credentials", "true"); +} + export const config = { matcher: "/:path*", }; From a3a88a41ae5d45e748dcb4166a6103f5c5c6a354 Mon Sep 17 00:00:00 2001 From: Flavien David Date: Thu, 20 Mar 2025 21:52:36 +0100 Subject: [PATCH 2/6] =?UTF-8?q?=E2=9C=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- front/middleware.ts | 31 ++++++------------------------- 1 file changed, 6 insertions(+), 25 deletions(-) diff --git a/front/middleware.ts b/front/middleware.ts index 785592a60451..d806c9764ef6 100644 --- a/front/middleware.ts +++ b/front/middleware.ts @@ -51,7 +51,7 @@ export function middleware(request: NextRequest) { }); } - // Handle CORS for public API endpoints + // Handle CORS only for public API endpoints. if (url.startsWith("/v1")) { if (request.method === "OPTIONS") { const response = new NextResponse(null, { status: 200 }); @@ -64,25 +64,16 @@ export function middleware(request: NextRequest) { return response; } - // Handle development environment CORS - if (process.env.NODE_ENV === "development") { - if (request.method === "OPTIONS") { - const response = new NextResponse(null, { status: 200 }); - setDevCorsHeaders(response); - return response; - } - - const response = NextResponse.next(); - setDevCorsHeaders(response); - return response; - } - return NextResponse.next(); } function setCorsHeaders(response: NextResponse, request: NextRequest) { const origin = request.headers.get("origin"); - if (origin && isAllowedOrigin(origin)) { + + if (process.env.NODE_ENV === "development" && origin === DEV_ORIGIN) { + response.headers.set("Access-Control-Allow-Origin", DEV_ORIGIN); + response.headers.set("Access-Control-Allow-Credentials", "true"); + } else if (origin && isAllowedOrigin(origin)) { response.headers.set("Access-Control-Allow-Origin", origin); response.headers.set("Access-Control-Allow-Credentials", "true"); } @@ -97,16 +88,6 @@ function setCorsHeaders(response: NextResponse, request: NextRequest) { ); } -function setDevCorsHeaders(response: NextResponse) { - response.headers.set("Access-Control-Allow-Origin", DEV_ORIGIN); - response.headers.set("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); - response.headers.set( - "Access-Control-Allow-Headers", - "Authorization, X-Request-Origin, x-Commit-Hash, X-Dust-Extension-Version, Content-Type" - ); - response.headers.set("Access-Control-Allow-Credentials", "true"); -} - export const config = { matcher: "/:path*", }; From 70cb19fd4b47997f4736c7960e1b880410bafdf6 Mon Sep 17 00:00:00 2001 From: Flavien David Date: Thu, 20 Mar 2025 21:53:09 +0100 Subject: [PATCH 3/6] =?UTF-8?q?=E2=9C=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- front/middleware.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/front/middleware.ts b/front/middleware.ts index d806c9764ef6..8d4b051b483f 100644 --- a/front/middleware.ts +++ b/front/middleware.ts @@ -2,6 +2,7 @@ import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; import { DEV_ORIGIN, isAllowedOrigin } from "@app/config/cors"; +import { isDevelopment } from "@app/types"; export function middleware(request: NextRequest) { const url = request.nextUrl.pathname; @@ -70,7 +71,7 @@ export function middleware(request: NextRequest) { function setCorsHeaders(response: NextResponse, request: NextRequest) { const origin = request.headers.get("origin"); - if (process.env.NODE_ENV === "development" && origin === DEV_ORIGIN) { + if (isDevelopment() && origin === DEV_ORIGIN) { response.headers.set("Access-Control-Allow-Origin", DEV_ORIGIN); response.headers.set("Access-Control-Allow-Credentials", "true"); } else if (origin && isAllowedOrigin(origin)) { From 297448c19900ea2b402848fc78a6e028c0546cb0 Mon Sep 17 00:00:00 2001 From: Flavien David Date: Thu, 20 Mar 2025 22:02:44 +0100 Subject: [PATCH 4/6] =?UTF-8?q?=E2=9C=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- front/middleware.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/front/middleware.ts b/front/middleware.ts index 8d4b051b483f..59ec9e492feb 100644 --- a/front/middleware.ts +++ b/front/middleware.ts @@ -53,7 +53,7 @@ export function middleware(request: NextRequest) { } // Handle CORS only for public API endpoints. - if (url.startsWith("/v1")) { + if (url.startsWith("/api/v1")) { if (request.method === "OPTIONS") { const response = new NextResponse(null, { status: 200 }); setCorsHeaders(response, request); @@ -71,7 +71,9 @@ export function middleware(request: NextRequest) { function setCorsHeaders(response: NextResponse, request: NextRequest) { const origin = request.headers.get("origin"); - if (isDevelopment() && origin === DEV_ORIGIN) { + // Cannot use helper functions like isDevelopment() in Edge Runtime middleware + // since they are not bundled. Must check NODE_ENV directly. + if (process.env.NODE_ENV === "development" && origin === DEV_ORIGIN) { response.headers.set("Access-Control-Allow-Origin", DEV_ORIGIN); response.headers.set("Access-Control-Allow-Credentials", "true"); } else if (origin && isAllowedOrigin(origin)) { From 6857eadbdce03e93d073e6153e115d573c7d6241 Mon Sep 17 00:00:00 2001 From: Flavien David Date: Thu, 20 Mar 2025 22:26:50 +0100 Subject: [PATCH 5/6] Improve CORS handling --- front/config/cors.ts | 13 +++++++ front/middleware.ts | 82 ++++++++++++++++++++++++++++++++++++-------- 2 files changed, 81 insertions(+), 14 deletions(-) diff --git a/front/config/cors.ts b/front/config/cors.ts index c5ebeda7fbbd..90c2b751f0f1 100644 --- a/front/config/cors.ts +++ b/front/config/cors.ts @@ -6,3 +6,16 @@ export function isAllowedOrigin(origin: string): origin is AllowedOriginType { } export const DEV_ORIGIN = "http://localhost:3010" as const; + +export const ALLOWED_HEADERS = [ + "authorization", + "content-type", + "x-request-origin", + "x-commit-hash", + "x-dust-extension-version", +] as const; +type AllowedHeaderType = (typeof ALLOWED_HEADERS)[number]; + +export function isAllowedHeader(header: string): header is AllowedHeaderType { + return ALLOWED_HEADERS.includes(header as AllowedHeaderType); +} diff --git a/front/middleware.ts b/front/middleware.ts index 59ec9e492feb..0149c4814e65 100644 --- a/front/middleware.ts +++ b/front/middleware.ts @@ -1,8 +1,12 @@ import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; -import { DEV_ORIGIN, isAllowedOrigin } from "@app/config/cors"; -import { isDevelopment } from "@app/types"; +import { + ALLOWED_HEADERS, + DEV_ORIGIN, + isAllowedHeader, + isAllowedOrigin, +} from "@app/config/cors"; export function middleware(request: NextRequest) { const url = request.nextUrl.pathname; @@ -55,30 +59,78 @@ export function middleware(request: NextRequest) { // Handle CORS only for public API endpoints. if (url.startsWith("/api/v1")) { if (request.method === "OPTIONS") { + // Handle preflight request. const response = new NextResponse(null, { status: 200 }); - setCorsHeaders(response, request); - return response; + return handleCors(response, request); } + // Handle actual request. const response = NextResponse.next(); - setCorsHeaders(response, request); - return response; + return handleCors(response, request); } return NextResponse.next(); } -function setCorsHeaders(response: NextResponse, request: NextRequest) { +function handleCors( + response: NextResponse, + request: NextRequest +): NextResponse { + const corsResponse = setCorsHeaders(response, request); + if (corsResponse) { + // If setCorsHeaders returned a response, it's an error. + return corsResponse; + } + + return response; +} + +function setCorsHeaders( + response: NextResponse, + request: NextRequest +): NextResponse | undefined { const origin = request.headers.get("origin"); + const requestHeaders = request.headers + .get("access-control-request-headers") + ?.toLowerCase(); - // Cannot use helper functions like isDevelopment() in Edge Runtime middleware - // since they are not bundled. Must check NODE_ENV directly. - if (process.env.NODE_ENV === "development" && origin === DEV_ORIGIN) { - response.headers.set("Access-Control-Allow-Origin", DEV_ORIGIN); - response.headers.set("Access-Control-Allow-Credentials", "true"); - } else if (origin && isAllowedOrigin(origin)) { + // If this is a preflight request checking headers. + if (request.method === "OPTIONS" && requestHeaders) { + const requestedHeaders = requestHeaders.split(",").map((h) => h.trim()); + const hasUnallowedHeader = requestedHeaders.some( + (header) => !isAllowedHeader(header) + ); + + if (hasUnallowedHeader) { + return new NextResponse(null, { + status: 403, + statusText: "Forbidden: Unauthorized Headers", + }); + } + } + + // Check origin. + if (!origin) { + return new NextResponse(null, { + status: 403, + statusText: "Forbidden: Missing Origin", + }); + } + + // Check if origin is allowed (prod or dev). + + // Cannot use helper functions like isDevelopment() in Edge Runtime middleware since they are not + // bundled. Must check NODE_ENV directly. + const isDevOrigin = + process.env.NODE_ENV === "development" && origin === DEV_ORIGIN; + if (isDevOrigin || isAllowedOrigin(origin)) { response.headers.set("Access-Control-Allow-Origin", origin); response.headers.set("Access-Control-Allow-Credentials", "true"); + } else { + return new NextResponse(null, { + status: 403, + statusText: "Forbidden: Unauthorized Origin", + }); } response.headers.set( @@ -87,8 +139,10 @@ function setCorsHeaders(response: NextResponse, request: NextRequest) { ); response.headers.set( "Access-Control-Allow-Headers", - "Authorization, Content-Type, X-Request-Origin, x-Commit-Hash, X-Dust-Extension-Version" + ALLOWED_HEADERS.join(", ") ); + + return undefined; } export const config = { From bffa4a156b45117b038633e4cfb1a1946778c2cb Mon Sep 17 00:00:00 2001 From: Flavien David Date: Fri, 21 Mar 2025 10:16:45 +0100 Subject: [PATCH 6/6] Feedback --- front/config/cors.ts | 5 ++--- front/middleware.ts | 12 +++++------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/front/config/cors.ts b/front/config/cors.ts index 90c2b751f0f1..56356bc2f2d2 100644 --- a/front/config/cors.ts +++ b/front/config/cors.ts @@ -5,14 +5,13 @@ export function isAllowedOrigin(origin: string): origin is AllowedOriginType { return ALLOWED_ORIGINS.includes(origin as AllowedOriginType); } -export const DEV_ORIGIN = "http://localhost:3010" as const; - export const ALLOWED_HEADERS = [ "authorization", "content-type", - "x-request-origin", "x-commit-hash", "x-dust-extension-version", + "x-hackerone-research", + "x-request-origin", ] as const; type AllowedHeaderType = (typeof ALLOWED_HEADERS)[number]; diff --git a/front/middleware.ts b/front/middleware.ts index 0149c4814e65..0cd7f22dcf41 100644 --- a/front/middleware.ts +++ b/front/middleware.ts @@ -3,7 +3,6 @@ import { NextResponse } from "next/server"; import { ALLOWED_HEADERS, - DEV_ORIGIN, isAllowedHeader, isAllowedOrigin, } from "@app/config/cors"; @@ -76,10 +75,10 @@ function handleCors( response: NextResponse, request: NextRequest ): NextResponse { - const corsResponse = setCorsHeaders(response, request); - if (corsResponse) { + const corsResponseError = setCorsHeaders(response, request); + if (corsResponseError) { // If setCorsHeaders returned a response, it's an error. - return corsResponse; + return corsResponseError; } return response; @@ -121,9 +120,8 @@ function setCorsHeaders( // Cannot use helper functions like isDevelopment() in Edge Runtime middleware since they are not // bundled. Must check NODE_ENV directly. - const isDevOrigin = - process.env.NODE_ENV === "development" && origin === DEV_ORIGIN; - if (isDevOrigin || isAllowedOrigin(origin)) { + const isDevelopment = process.env.NODE_ENV === "development"; + if (isDevelopment || isAllowedOrigin(origin)) { response.headers.set("Access-Control-Allow-Origin", origin); response.headers.set("Access-Control-Allow-Credentials", "true"); } else {