Skip to content

Commit 63f0b97

Browse files
committed
fix(proxy): restrict gateway bypass to loopback IPs
1 parent c01f594 commit 63f0b97

4 files changed

Lines changed: 38 additions & 8 deletions

File tree

src/gateway/client.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { dangerouslyBypassManagedProxyForGatewayLoopbackControlPlane } from "../
1717
import { normalizeFingerprint } from "../infra/tls/fingerprint.js";
1818
import { rawDataToString } from "../infra/ws.js";
1919
import { logDebug, logError } from "../logger.js";
20+
import { isLoopbackIpAddress } from "../shared/net/ip.js";
2021
import {
2122
normalizeLowercaseStringOrEmpty,
2223
normalizeOptionalString,
@@ -99,7 +100,7 @@ function createDirectGatewayAgent(url: string): http.Agent | https.Agent | undef
99100
} catch {
100101
return undefined;
101102
}
102-
if (!isLoopbackHost(hostname)) {
103+
if (!isLoopbackIpAddress(hostname)) {
103104
return undefined;
104105
}
105106
return url.startsWith("wss://") ? new https.Agent() : new http.Agent();

src/gateway/gateway-misc.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,22 @@ describe("GatewayClient", () => {
9797
);
9898
});
9999

100+
test("uses an explicit direct agent for IPv6 loopback control-plane WebSocket connections", () => {
101+
const client = new GatewayClient({ url: "ws://[::1]:1" });
102+
client.start();
103+
const last = wsMockState.last as { opts: { agent?: unknown } } | null;
104+
105+
expect(last?.opts.agent).toBeDefined();
106+
});
107+
108+
test("does not use the direct control-plane bypass for localhost hostnames", () => {
109+
const client = new GatewayClient({ url: "ws://localhost:1" });
110+
client.start();
111+
const last = wsMockState.last as { opts: { agent?: unknown } } | null;
112+
113+
expect(last?.opts.agent).toBeUndefined();
114+
});
115+
100116
test("does not force a direct agent for remote Gateway WebSocket connections", () => {
101117
const client = new GatewayClient({
102118
url: "wss://gateway.example.com",

src/infra/net/proxy/proxy-lifecycle.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,24 @@ describe("startProxy", () => {
332332
await stopProxy(handle);
333333
});
334334

335+
it("allows the Gateway control-plane bypass for literal loopback IPs only", () => {
336+
expect(
337+
dangerouslyBypassManagedProxyForGatewayLoopbackControlPlane(
338+
"ws://127.0.0.1:18789",
339+
() => "ok",
340+
),
341+
).toBe("ok");
342+
expect(
343+
dangerouslyBypassManagedProxyForGatewayLoopbackControlPlane("ws://[::1]:18789", () => "ok"),
344+
).toBe("ok");
345+
expect(() =>
346+
dangerouslyBypassManagedProxyForGatewayLoopbackControlPlane(
347+
"ws://localhost:18789",
348+
() => undefined,
349+
),
350+
).toThrow("loopback-only");
351+
});
352+
335353
it("rejects dangerous Gateway control-plane bypass for non-loopback URLs", () => {
336354
expect(() =>
337355
dangerouslyBypassManagedProxyForGatewayLoopbackControlPlane(

src/infra/net/proxy/proxy-lifecycle.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import https from "node:https";
1212
import { bootstrap as bootstrapGlobalAgent } from "global-agent";
1313
import type { ProxyConfig } from "../../../config/zod-schema.proxy.js";
1414
import { logInfo, logWarn } from "../../../logger.js";
15+
import { isLoopbackIpAddress } from "../../../shared/net/ip.js";
1516
import { forceResetGlobalDispatcher } from "../undici-global-dispatcher.js";
1617

1718
export type ProxyHandle = {
@@ -287,13 +288,7 @@ function isGatewayLoopbackControlPlaneUrl(value: string): boolean {
287288
) {
288289
return false;
289290
}
290-
const hostname = url.hostname.toLowerCase();
291-
return (
292-
hostname === "localhost" ||
293-
hostname === "127.0.0.1" ||
294-
hostname === "::1" ||
295-
hostname === "[::1]"
296-
);
291+
return isLoopbackIpAddress(url.hostname);
297292
}
298293

299294
export function dangerouslyBypassManagedProxyForGatewayLoopbackControlPlane<T>(

0 commit comments

Comments
 (0)