From 652fa53e4912837fe234651e7c7705fb35abe188 Mon Sep 17 00:00:00 2001 From: Gustavo Valverde Date: Thu, 11 Jun 2026 21:54:05 -0700 Subject: [PATCH] fix(auth)!: ignore x-forwarded headers by default on dynamic baseURL (#9134) --- .changeset/proxy-trust-default-dynamic.md | 20 ++++++++++++++++++ .../src/api/to-auth-endpoints.test.ts | 21 +++++++++++++++++++ packages/better-auth/src/context/helpers.ts | 9 ++++---- 3 files changed, 46 insertions(+), 4 deletions(-) create mode 100644 .changeset/proxy-trust-default-dynamic.md diff --git a/.changeset/proxy-trust-default-dynamic.md b/.changeset/proxy-trust-default-dynamic.md new file mode 100644 index 0000000000..9cb830b44e --- /dev/null +++ b/.changeset/proxy-trust-default-dynamic.md @@ -0,0 +1,20 @@ +--- +"better-auth": minor +--- + +The dynamic `baseURL` config now ignores `x-forwarded-host` and `x-forwarded-proto` unless you set `advanced.trustedProxyHeaders: true`. + +Requests using `baseURL: { allowedHosts }` now resolve the auth origin from `Host` by default, so forwarded headers cannot select another allowed host unless trusted proxy headers are enabled. + +**Breaking change:** if your proxy exposes the public hostname only through `x-forwarded-host`, set `advanced.trustedProxyHeaders: true`. Deployments where the proxy rewrites `Host` to the public hostname (nginx default, Vercel, Cloudflare, and Netlify) are unaffected. + +**Migration:** + +```ts +betterAuth({ + baseURL: { allowedHosts: [...] }, + advanced: { + trustedProxyHeaders: true, + }, +}); +``` diff --git a/packages/better-auth/src/api/to-auth-endpoints.test.ts b/packages/better-auth/src/api/to-auth-endpoints.test.ts index e7e674f7b0..da1cf10dd5 100644 --- a/packages/better-auth/src/api/to-auth-endpoints.test.ts +++ b/packages/better-auth/src/api/to-auth-endpoints.test.ts @@ -1029,6 +1029,27 @@ describe("dynamic baseURL resolution", () => { expect(sharedCtx.baseURL).toBe(""); }); + /** + * @see https://github.com/better-auth/better-auth/pull/9134 + */ + it("should ignore x-forwarded headers when trustedProxyHeaders is not configured", async () => { + const authContext = init({ + baseURL: { + allowedHosts: ["example.com", "proxy.example.com"], + }, + }); + const authEndpoints = toAuthEndpoints(endpoints, authContext); + + const res = await authEndpoints.readBaseURL({ + headers: new Headers({ + host: "example.com", + "x-forwarded-host": "proxy.example.com", + "x-forwarded-proto": "http", + }), + }); + expect(res.baseURL).toBe("https://example.com/api/auth"); + }); + it("should ignore x-forwarded-host when trustedProxyHeaders is explicitly disabled", async () => { const authContext = init({ baseURL: { diff --git a/packages/better-auth/src/context/helpers.ts b/packages/better-auth/src/context/helpers.ts index 85ad10ba5f..50c8d62cd1 100644 --- a/packages/better-auth/src/context/helpers.ts +++ b/packages/better-auth/src/context/helpers.ts @@ -190,14 +190,15 @@ export function pickSource( /** * Returns the effective `trustedProxyHeaders` value for dynamic `baseURL` - * resolution. When the user hasn't set `advanced.trustedProxyHeaders`, - * proxy headers (`x-forwarded-host` / `x-forwarded-proto`) are trusted by - * default so deployments behind a reverse proxy work without extra config. + * resolution. Proxy headers (`x-forwarded-host` / `x-forwarded-proto`) are + * ignored unless the user opts in with `advanced.trustedProxyHeaders: true`, + * matching the static-config path. Deployments behind a reverse proxy that + * only exposes the public host via `x-forwarded-host` must set the flag. */ export function resolveDynamicTrustedProxyHeaders( options: BetterAuthOptions, ): boolean { - return options.advanced?.trustedProxyHeaders ?? true; + return options.advanced?.trustedProxyHeaders ?? false; } /**