fix: keep tui out of browser origin checks

This commit is contained in:
Shakker
2026-03-27 10:09:18 +00:00
committed by Shakker
parent 2b96569e2d
commit f1de00c107
4 changed files with 35 additions and 4 deletions

View File

@@ -224,6 +224,32 @@ describe("gateway auth browser hardening", () => {
});
});
test("rejects browser-origin connects that claim to be tui clients", async () => {
testState.gatewayAuth = { mode: "token", token: "secret" };
await withGatewayServer(async ({ port }) => {
const ws = await openWs(port, { origin: "https://attacker.example" });
try {
const res = await connectReq(ws, {
token: "secret",
client: {
id: GATEWAY_CLIENT_NAMES.TUI,
version: "1.0.0",
platform: "darwin",
mode: GATEWAY_CLIENT_MODES.UI,
},
device: null,
});
expect(res.ok).toBe(false);
expect(res.error?.message ?? "").toContain("origin not allowed");
expect((res.error?.details as { code?: string } | undefined)?.code).toBe(
ConnectErrorDetailCodes.CONTROL_UI_ORIGIN_NOT_ALLOWED,
);
} finally {
ws.close();
}
});
});
test("rate-limits browser-origin auth failures on loopback even when loopback exemption is enabled", async () => {
testState.gatewayAuth = {
mode: "token",

View File

@@ -299,9 +299,7 @@ export function registerControlUiAndPairingSuite(): void {
test("allows localhost tui without device identity when insecure auth is enabled", async () => {
testState.gatewayControlUi = { allowInsecureAuth: true };
const { server, ws, prevToken } = await startServerWithClient("secret", {
wsHeaders: { origin: "http://127.0.0.1" },
});
const { server, ws, prevToken } = await startServerWithClient("secret");
await connectControlUiWithoutDeviceAndExpectOk({
ws,
token: "secret",

View File

@@ -24,6 +24,7 @@ import { rawDataToString } from "../../../infra/ws.js";
import type { createSubsystemLogger } from "../../../logging/subsystem.js";
import { roleScopesAllow } from "../../../shared/operator-scope-compat.js";
import {
isBrowserOperatorUiClient,
isGatewayCliClient,
isOperatorUiClient,
isWebchatClient,
@@ -405,8 +406,9 @@ export function attachGatewayWsMessageHandler(params: {
connectParams.scopes = scopes;
const isControlUi = isOperatorUiClient(connectParams.client);
const isBrowserOperatorUi = isBrowserOperatorUiClient(connectParams.client);
const isWebchat = isWebchatConnect(connectParams);
if (enforceOriginCheckForAnyClient || isControlUi || isWebchat) {
if (enforceOriginCheckForAnyClient || isBrowserOperatorUi || isWebchat) {
const hostHeaderOriginFallbackEnabled =
configSnapshot.gateway?.controlUi?.dangerouslyAllowHostHeaderOriginFallback === true;
const originCheck = checkBrowserOrigin({

View File

@@ -47,6 +47,11 @@ export function isOperatorUiClient(client?: GatewayClientInfoLike | null): boole
return clientId === GATEWAY_CLIENT_NAMES.CONTROL_UI || clientId === GATEWAY_CLIENT_NAMES.TUI;
}
export function isBrowserOperatorUiClient(client?: GatewayClientInfoLike | null): boolean {
const clientId = normalizeGatewayClientName(client?.id);
return clientId === GATEWAY_CLIENT_NAMES.CONTROL_UI;
}
export function isInternalMessageChannel(raw?: string | null): raw is InternalMessageChannel {
return normalizeMessageChannel(raw) === INTERNAL_MESSAGE_CHANNEL;
}