From 8dfb38af22a9bda26ffc2310de88c2f841312467 Mon Sep 17 00:00:00 2001 From: rick Date: Tue, 3 Mar 2026 23:28:04 -0600 Subject: [PATCH] fix(gateway): tighten trusted-proxy local fallback --- src/gateway/auth.test.ts | 57 ++++++++++++++++++++++++++++++++++++++++ src/gateway/auth.ts | 14 +++++++++- 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/src/gateway/auth.test.ts b/src/gateway/auth.test.ts index 974e2a2074d..4012b6990c3 100644 --- a/src/gateway/auth.test.ts +++ b/src/gateway/auth.test.ts @@ -417,6 +417,63 @@ describe("trusted-proxy auth", () => { expect(localToken.method).toBe("token"); }); + it("does not let local fallback preempt valid trusted-proxy header auth", async () => { + const res = await authorizeGatewayConnect({ + auth: { + mode: "trusted-proxy", + allowTailscale: false, + token: "secret", + trustedProxy: trustedProxyConfig, + }, + connectAuth: { token: "wrong" }, + trustedProxies: ["127.0.0.1"], + req: { + socket: { remoteAddress: "127.0.0.1" }, + headers: { + host: "gateway.local", + "x-forwarded-for": "203.0.113.10", + "x-forwarded-user": "nick@example.com", + "x-forwarded-proto": "https", + }, + } as never, + }); + + expect(res.ok).toBe(true); + expect(res.method).toBe("trusted-proxy"); + expect(res.user).toBe("nick@example.com"); + }); + + it("applies rate limiting before trusted-proxy local shared-secret fallback", async () => { + const limiter = createLimiterSpy(); + limiter.check.mockReturnValue({ + allowed: false, + remaining: 0, + retryAfterMs: 60_000, + }); + + const res = await authorizeGatewayConnect({ + auth: { + mode: "trusted-proxy", + allowTailscale: false, + token: "secret", + trustedProxy: trustedProxyConfig, + }, + connectAuth: { token: "secret" }, + trustedProxies: ["127.0.0.1"], + req: { + socket: { remoteAddress: "127.0.0.1" }, + headers: { + host: "127.0.0.1:19001", + }, + } as never, + rateLimiter: limiter, + }); + + expect(res.ok).toBe(false); + expect(res.reason).toBe("rate_limited"); + expect(limiter.check).toHaveBeenCalled(); + }); + it("rejects request from untrusted source", async () => { const res = await authorizeTrustedProxy({ remoteAddress: "192.168.1.100", diff --git a/src/gateway/auth.ts b/src/gateway/auth.ts index b5f9e885ad9..de82ee3a33e 100644 --- a/src/gateway/auth.ts +++ b/src/gateway/auth.ts @@ -419,7 +419,19 @@ export async function authorizeGatewayConnect( !req?.headers?.["x-forwarded-host"]; if (auth.mode === "trusted-proxy") { - if (localDirect || localLoopbackWithoutProxyHeaders) { + if (localLoopbackWithoutProxyHeaders && limiter) { + const rlCheck: RateLimitCheckResult = limiter.check(ip, rateLimitScope); + if (!rlCheck.allowed) { + return { + ok: false, + reason: "rate_limited", + rateLimited: true, + retryAfterMs: rlCheck.retryAfterMs, + }; + } + } + + if (localLoopbackWithoutProxyHeaders) { const sharedSecretFallback = authorizeSharedSecretFallback({ auth, connectAuth,