fix: Gateway canvas host bypasses auth and serves files unauthenticated

This commit is contained in:
Coy Geek
2026-02-05 01:21:06 -08:00
committed by George Pickett
parent 05b28c147d
commit 47538bca4d
2 changed files with 72 additions and 8 deletions

View File

@@ -10,9 +10,15 @@ import { createServer as createHttpsServer } from "node:https";
import type { CanvasHostHandler } from "../canvas-host/server.js";
import type { createSubsystemLogger } from "../logging/subsystem.js";
import { resolveAgentAvatar } from "../agents/identity-avatar.js";
import { handleA2uiHttpRequest } from "../canvas-host/a2ui.js";
import {
A2UI_PATH,
CANVAS_HOST_PATH,
CANVAS_WS_PATH,
handleA2uiHttpRequest,
} from "../canvas-host/a2ui.js";
import { loadConfig } from "../config/config.js";
import { handleSlackHttpRequest } from "../slack/http/index.js";
import { authorizeGatewayConnect } from "./auth.js";
import {
handleControlUiAvatarRequest,
handleControlUiHttpRequest,
@@ -31,6 +37,8 @@ import {
resolveHookChannel,
resolveHookDeliver,
} from "./hooks.js";
import { sendUnauthorized } from "./http-common.js";
import { getBearerToken } from "./http-utils.js";
import { handleOpenAiHttpRequest } from "./openai-http.js";
import { handleOpenResponsesHttpRequest } from "./openresponses-http.js";
import { handleToolsInvokeHttpRequest } from "./tools-invoke-http.js";
@@ -60,6 +68,16 @@ function sendJson(res: ServerResponse, status: number, body: unknown) {
res.end(JSON.stringify(body));
}
function isCanvasPath(pathname: string): boolean {
return (
pathname === A2UI_PATH ||
pathname.startsWith(`${A2UI_PATH}/`) ||
pathname === CANVAS_HOST_PATH ||
pathname.startsWith(`${CANVAS_HOST_PATH}/`) ||
pathname === CANVAS_WS_PATH
);
}
export type HooksRequestHandler = (req: IncomingMessage, res: ServerResponse) => Promise<boolean>;
export function createHooksRequestHandler(
@@ -287,6 +305,20 @@ export function createGatewayHttpServer(opts: {
}
}
if (canvasHost) {
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
if (isCanvasPath(url.pathname)) {
const token = getBearerToken(req);
const authResult = await authorizeGatewayConnect({
auth: resolvedAuth,
connectAuth: token ? { token, password: token } : null,
req,
trustedProxies,
});
if (!authResult.ok) {
sendUnauthorized(res);
return;
}
}
if (await handleA2uiHttpRequest(req, res)) {
return;
}
@@ -331,14 +363,41 @@ export function attachGatewayUpgradeHandler(opts: {
httpServer: HttpServer;
wss: WebSocketServer;
canvasHost: CanvasHostHandler | null;
resolvedAuth: import("./auth.js").ResolvedGatewayAuth;
}) {
const { httpServer, wss, canvasHost } = opts;
const { httpServer, wss, canvasHost, resolvedAuth } = opts;
httpServer.on("upgrade", (req, socket, head) => {
if (canvasHost?.handleUpgrade(req, socket, head)) {
return;
}
wss.handleUpgrade(req, socket, head, (ws) => {
wss.emit("connection", ws, req);
void (async () => {
if (canvasHost) {
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
if (url.pathname === CANVAS_WS_PATH) {
const configSnapshot = loadConfig();
const token = getBearerToken(req);
const authResult = await authorizeGatewayConnect({
auth: resolvedAuth,
connectAuth: token ? { token, password: token } : null,
req,
trustedProxies: configSnapshot.gateway?.trustedProxies ?? [],
});
if (!authResult.ok) {
socket.write("HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n");
socket.destroy();
return;
}
}
}
if (canvasHost?.handleUpgrade(req, socket, head)) {
return;
}
wss.handleUpgrade(req, socket, head, (ws) => {
wss.emit("connection", ws, req);
});
})().catch(() => {
try {
socket.destroy();
} catch {
// ignore
}
});
});
}

View File

@@ -164,7 +164,12 @@ export async function createGatewayRuntimeState(params: {
maxPayload: MAX_PAYLOAD_BYTES,
});
for (const server of httpServers) {
attachGatewayUpgradeHandler({ httpServer: server, wss, canvasHost });
attachGatewayUpgradeHandler({
httpServer: server,
wss,
canvasHost,
resolvedAuth: params.resolvedAuth,
});
}
const clients = new Set<GatewayWsClient>();