diff --git a/env/index.ts b/env/index.ts index 95aee6c8b..83e637e95 100644 --- a/env/index.ts +++ b/env/index.ts @@ -1,6 +1,7 @@ export type Env = { [key: string]: unknown; FLY_APP_NAME?: string; + VERCEL?: string; NODE_ENV?: string; ARCJET_KEY?: string; ARCJET_ENV?: string; @@ -12,6 +13,10 @@ export function platform(env: Env) { if (typeof env["FLY_APP_NAME"] === "string" && env["FLY_APP_NAME"] !== "") { return "fly-io" as const; } + + if (typeof env["VERCEL"] === "string" && env["VERCEL"] === "1") { + return "vercel" as const; + } } export function isDevelopment(env: Env) { diff --git a/ip/index.ts b/ip/index.ts index 384f478d7..d48bfe5a7 100644 --- a/ip/index.ts +++ b/ip/index.ts @@ -553,7 +553,7 @@ export interface RequestLike { requestContext?: PartialRequestContext; } -export type Platform = "cloudflare" | "fly-io"; +export type Platform = "cloudflare" | "fly-io" | "vercel"; export interface Options { platform?: Platform; @@ -649,6 +649,55 @@ function findIP( return ""; } + if (platform === "vercel") { + // https://vercel.com/docs/edge-network/headers/request-headers#x-real-ip + // Also used by `@vercel/functions`, see: + // https://github.com/vercel/vercel/blob/d7536d52c87712b1b3f83e4b0fd535a1fb7e384c/packages/functions/src/headers.ts#L12 + const xRealIP = headers.get("x-real-ip"); + if (isGlobalIP(xRealIP)) { + return xRealIP; + } + + // https://vercel.com/docs/edge-network/headers/request-headers#x-vercel-forwarded-for + // By default, it seems this will be 1 address, but they discuss trusted + // proxy forwarding so we try to parse it like normal. See + // https://vercel.com/docs/edge-network/headers/request-headers#custom-x-forwarded-for-ip + const xVercelForwardedFor = headers.get("x-vercel-forwarded-for"); + const xVercelForwardedForItems = parseXForwardedFor(xVercelForwardedFor); + // As per MDN X-Forwarded-For Headers documentation at + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For + // We may find more than one IP in the `x-forwarded-for` header. Since the + // first IP will be closest to the user (and the most likely to be spoofed), + // we want to iterate tail-to-head so we reverse the list. + for (const item of xVercelForwardedForItems.reverse()) { + if (isGlobalIP(item)) { + return item; + } + } + + // https://vercel.com/docs/edge-network/headers/request-headers#x-forwarded-for + // By default, it seems this will be 1 address, but they discuss trusted + // proxy forwarding so we try to parse it like normal. See + // https://vercel.com/docs/edge-network/headers/request-headers#custom-x-forwarded-for-ip + const xForwardedFor = headers.get("x-forwarded-for"); + const xForwardedForItems = parseXForwardedFor(xForwardedFor); + // As per MDN X-Forwarded-For Headers documentation at + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For + // We may find more than one IP in the `x-forwarded-for` header. Since the + // first IP will be closest to the user (and the most likely to be spoofed), + // we want to iterate tail-to-head so we reverse the list. + for (const item of xForwardedForItems.reverse()) { + if (isGlobalIP(item)) { + return item; + } + } + + // If we are using a platform check and don't have a Global IP, we exit + // early with an empty IP since the more generic headers shouldn't be + // trusted over the platform-specific headers. + return ""; + } + // Standard headers used by Amazon EC2, Heroku, and others. const xClientIP = headers.get("x-client-ip"); if (isGlobalIP(xClientIP)) { diff --git a/ip/test/ipv4.test.ts b/ip/test/ipv4.test.ts index 9d9f55432..f2420baa7 100644 --- a/ip/test/ipv4.test.ts +++ b/ip/test/ipv4.test.ts @@ -173,6 +173,9 @@ describe("find public IPv4", () => { headerSuite("X-Client-IP"); headerSuite("X-Forwarded-For"); headerSuite("CF-Connecting-IP", { platform: "cloudflare" }); + headerSuite("X-Real-IP", { platform: "vercel" }); + headerSuite("X-Vercel-Forwarded-For", { platform: "vercel" }); + headerSuite("X-Forwarded-For", { platform: "vercel" }); headerSuite("DO-Connecting-IP"); headerSuite("Fastly-Client-IP"); headerSuite("Fly-Client-IP", { platform: "fly-io" }); diff --git a/ip/test/ipv6.test.ts b/ip/test/ipv6.test.ts index bf2e31341..e9a549627 100644 --- a/ip/test/ipv6.test.ts +++ b/ip/test/ipv6.test.ts @@ -151,6 +151,9 @@ describe("find public IPv6", () => { headerSuite("X-Forwarded-For"); headerSuite("CF-Connecting-IPv6", { platform: "cloudflare" }); headerSuite("CF-Connecting-IP", { platform: "cloudflare" }); + headerSuite("X-Real-IP", { platform: "vercel" }); + headerSuite("X-Vercel-Forwarded-For", { platform: "vercel" }); + headerSuite("X-Forwarded-For", { platform: "vercel" }); headerSuite("DO-Connecting-IP"); headerSuite("Fastly-Client-IP"); headerSuite("Fly-Client-IP", { platform: "fly-io" });