From 07039dc089e51589a213ec0d16f8d6f2cd871fa1 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Fri, 20 Feb 2026 23:59:20 -0500 Subject: [PATCH] Gateway: harden trusted proxy X-Forwarded-For parsing (#22429) --- CHANGELOG.md | 1 + src/gateway/net.test.ts | 11 ++++++++++- src/gateway/net.ts | 6 +++++- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d71b4798b..a4e182216 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -108,6 +108,7 @@ Docs: https://docs.openclaw.ai - iOS/Watch: refresh iOS and watch app icon assets with the lobster icon set to keep phone/watch branding aligned. (#21997) Thanks @mbelinky. - CLI/Onboarding: fix Anthropic-compatible custom provider verification by normalizing base URLs to avoid duplicate `/v1` paths during setup checks. (#21336) Thanks @17jmumford. - Security/Dependencies: bump transitive `hono` usage to `4.11.10` to incorporate timing-safe authentication comparison hardening for `basicAuth`/`bearerAuth` (`GHSA-gq3j-xvxp-8hrf`). Thanks @vincentkoc. +- Security/Gateway: parse `X-Forwarded-For` with trust-preserving semantics when requests come from configured trusted proxies, preventing proxy-chain spoofing from influencing client IP classification and rate-limit identity. Thanks @AnthonyDiSanti and @vincentkoc. - iOS/Gateway/Tools: prefer uniquely connected node matches when duplicate display names exist, surface actionable `nodes invoke` pairing-required guidance with request IDs, and refresh active iOS gateway registration after location-capability setting changes so capability updates apply immediately. (#22120) thanks @mbelinky. ## 2026.2.19 diff --git a/src/gateway/net.test.ts b/src/gateway/net.test.ts index 8b37b4413..b91d45b6b 100644 --- a/src/gateway/net.test.ts +++ b/src/gateway/net.test.ts @@ -145,12 +145,21 @@ describe("resolveGatewayClientIp", () => { it("returns forwarded client IP when the remote is a trusted proxy", () => { const ip = resolveGatewayClientIp({ remoteAddr: "127.0.0.1", - forwardedFor: "10.0.0.2, 127.0.0.1", + forwardedFor: "127.0.0.1, 10.0.0.2", trustedProxies: ["127.0.0.1"], }); expect(ip).toBe("10.0.0.2"); }); + it("does not trust the left-most X-Forwarded-For value when behind a trusted proxy", () => { + const ip = resolveGatewayClientIp({ + remoteAddr: "127.0.0.1", + forwardedFor: "198.51.100.99, 10.0.0.9, 127.0.0.1", + trustedProxies: ["127.0.0.1"], + }); + expect(ip).toBe("127.0.0.1"); + }); + it("fails closed when trusted proxy headers are missing", () => { const ip = resolveGatewayClientIp({ remoteAddr: "127.0.0.1", diff --git a/src/gateway/net.ts b/src/gateway/net.ts index 28ad98027..b484f9f0d 100644 --- a/src/gateway/net.ts +++ b/src/gateway/net.ts @@ -147,7 +147,11 @@ function stripOptionalPort(ip: string): string { } export function parseForwardedForClientIp(forwardedFor?: string): string | undefined { - const raw = forwardedFor?.split(",")[0]?.trim(); + const entries = forwardedFor + ?.split(",") + .map((entry) => entry.trim()) + .filter((entry) => entry.length > 0); + const raw = entries?.at(-1); if (!raw) { return undefined; }