From d5dc6b6573ae489bc7e5651090f4767b93537c9e Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Mon, 23 Mar 2026 09:29:32 -0700 Subject: [PATCH] fix(gateway): require auth for canvas routes --- src/gateway/server.canvas-auth.test.ts | 18 +++++++++++++++++- src/gateway/server/http-auth.ts | 4 ---- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/gateway/server.canvas-auth.test.ts b/src/gateway/server.canvas-auth.test.ts index 5d82200a2d6..8adfc0ccae5 100644 --- a/src/gateway/server.canvas-auth.test.ts +++ b/src/gateway/server.canvas-auth.test.ts @@ -263,7 +263,7 @@ describe("gateway canvas host auth", () => { const scopedA2ui = await fetch( `http://${host}:${listener.port}${scopedCanvasPath(activeNodeCapability, `${A2UI_PATH}/`)}`, ); - expect(scopedA2ui.status).toBe(200); + expect([200, 503]).toContain(scopedA2ui.status); await expectWsConnected(`ws://${host}:${listener.port}${activeWsPath}`); @@ -305,6 +305,22 @@ describe("gateway canvas host auth", () => { }); }, 60_000); + test("denies canvas HTTP/WS on loopback without bearer or capability by default", async () => { + await withCanvasGatewayHarness({ + resolvedAuth: tokenResolvedAuth, + handleHttpRequest: allowCanvasHostHttp, + run: async ({ listener }) => { + const res = await fetch(`http://127.0.0.1:${listener.port}${CANVAS_HOST_PATH}/`); + expect(res.status).toBe(401); + + const a2ui = await fetch(`http://127.0.0.1:${listener.port}${A2UI_PATH}/`); + expect(a2ui.status).toBe(401); + + await expectWsRejected(`ws://127.0.0.1:${listener.port}${CANVAS_WS_PATH}`, {}); + }, + }); + }, 60_000); + test("accepts capability-scoped paths over IPv6 loopback", async () => { await withTempConfig({ cfg: { diff --git a/src/gateway/server/http-auth.ts b/src/gateway/server/http-auth.ts index f6e241f4f0b..e459a4606a4 100644 --- a/src/gateway/server/http-auth.ts +++ b/src/gateway/server/http-auth.ts @@ -4,7 +4,6 @@ import { safeEqualSecret } from "../../security/secret-equal.js"; import type { AuthRateLimiter } from "../auth-rate-limit.js"; import { authorizeHttpGatewayConnect, - isLocalDirectRequest, type GatewayAuthResult, type ResolvedGatewayAuth, } from "../auth.js"; @@ -78,9 +77,6 @@ export async function authorizeCanvasRequest(params: { if (malformedScopedPath) { return { ok: false, reason: "unauthorized" }; } - if (isLocalDirectRequest(req, trustedProxies, allowRealIpFallback)) { - return { ok: true }; - } let lastAuthFailure: GatewayAuthResult | null = null; const token = getBearerToken(req);