fix: Control UI Insecure Auth Bypass Allows Token-Only Auth Over HTTP (#20684)

Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: ad9be4b4d6
Co-authored-by: coygeek <65363919+coygeek@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
This commit is contained in:
Coy Geek
2026-02-20 09:34:34 -08:00
committed by GitHub
parent fe3215092c
commit 40a292619e
4 changed files with 32 additions and 7 deletions

View File

@@ -687,7 +687,7 @@ describe("gateway server auth/connect", () => {
});
});
test("allows control ui without device identity when insecure auth is enabled", async () => {
test("rejects control ui without device identity even when insecure auth is enabled", async () => {
testState.gatewayControlUi = { allowInsecureAuth: true };
const { server, ws, prevToken } = await startServerWithClient("secret", {
wsHeaders: { origin: "http://127.0.0.1" },
@@ -702,13 +702,32 @@ describe("gateway server auth/connect", () => {
mode: GATEWAY_CLIENT_MODES.WEBCHAT,
},
});
expect(res.ok).toBe(true);
expect(res.ok).toBe(false);
expect(res.error?.message ?? "").toContain("secure context");
ws.close();
await server.close();
restoreGatewayToken(prevToken);
});
test("allows control ui with device identity when insecure auth is enabled", async () => {
test("rejects control ui password-only auth when insecure auth is enabled", async () => {
testState.gatewayControlUi = { allowInsecureAuth: true };
testState.gatewayAuth = { mode: "password", password: "secret" };
await withGatewayServer(async ({ port }) => {
const ws = await openWs(port, { origin: originForPort(port) });
const res = await connectReq(ws, {
password: "secret",
device: null,
client: {
...CONTROL_UI_CLIENT,
},
});
expect(res.ok).toBe(false);
expect(res.error?.message ?? "").toContain("secure context");
ws.close();
});
});
test("does not bypass pairing for control ui device identity when insecure auth is enabled", async () => {
testState.gatewayControlUi = { allowInsecureAuth: true };
testState.gatewayAuth = { mode: "token", token: "secret" };
const { writeConfigFile } = await import("../config/config.js");
@@ -753,7 +772,8 @@ describe("gateway server auth/connect", () => {
...CONTROL_UI_CLIENT,
},
});
expect(res.ok).toBe(true);
expect(res.ok).toBe(false);
expect(res.error?.message ?? "").toContain("pairing required");
ws.close();
});
} finally {

View File

@@ -341,7 +341,9 @@ export function attachGatewayWsMessageHandler(params: {
isControlUi && configSnapshot.gateway?.controlUi?.allowInsecureAuth === true;
const disableControlUiDeviceAuth =
isControlUi && configSnapshot.gateway?.controlUi?.dangerouslyDisableDeviceAuth === true;
const allowControlUiBypass = allowInsecureControlUi || disableControlUiDeviceAuth;
// `allowInsecureAuth` is retained for compatibility, but must not bypass
// secure-context/device-auth requirements.
const allowControlUiBypass = disableControlUiDeviceAuth;
const device = disableControlUiDeviceAuth ? null : deviceRaw;
const hasDeviceTokenCandidate = Boolean(connectParams.auth?.token && device);
@@ -428,7 +430,9 @@ export function attachGatewayWsMessageHandler(params: {
if (isControlUi && !allowControlUiBypass) {
const errorMessage = "control ui requires HTTPS or localhost (secure context)";
markHandshakeFailure("control-ui-insecure-auth");
markHandshakeFailure("control-ui-insecure-auth", {
insecureAuthConfigured: allowInsecureControlUi,
});
sendHandshakeErrorResponse(ErrorCodes.INVALID_REQUEST, errorMessage);
close(1008, errorMessage);
return;