mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 11:17:40 +00:00
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:
@@ -13,6 +13,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
|
||||||
|
- Gateway/Security: require secure context and paired-device checks for Control UI auth even when `gateway.controlUi.allowInsecureAuth` is set, and align audit messaging with the hardened behavior. (#20684) thanks @coygeek.
|
||||||
- macOS/Build: default release packaging to `BUNDLE_ID=ai.openclaw.mac` in `scripts/package-mac-dist.sh`, so Sparkle feed URL is retained and auto-update no longer fails with an empty appcast feed. (#19750) thanks @loganprit.
|
- macOS/Build: default release packaging to `BUNDLE_ID=ai.openclaw.mac` in `scripts/package-mac-dist.sh`, so Sparkle feed URL is retained and auto-update no longer fails with an empty appcast feed. (#19750) thanks @loganprit.
|
||||||
|
|
||||||
- Signal/Outbound: preserve case for Base64 group IDs during outbound target normalization so cross-context routing and policy checks no longer break when group IDs include uppercase characters. (#5578) Thanks @heyhudson.
|
- Signal/Outbound: preserve case for Base64 group IDs during outbound target normalization so cross-context routing and policy checks no longer break when group IDs include uppercase characters. (#5578) Thanks @heyhudson.
|
||||||
|
|||||||
@@ -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 };
|
testState.gatewayControlUi = { allowInsecureAuth: true };
|
||||||
const { server, ws, prevToken } = await startServerWithClient("secret", {
|
const { server, ws, prevToken } = await startServerWithClient("secret", {
|
||||||
wsHeaders: { origin: "http://127.0.0.1" },
|
wsHeaders: { origin: "http://127.0.0.1" },
|
||||||
@@ -702,13 +702,32 @@ describe("gateway server auth/connect", () => {
|
|||||||
mode: GATEWAY_CLIENT_MODES.WEBCHAT,
|
mode: GATEWAY_CLIENT_MODES.WEBCHAT,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(res.ok).toBe(true);
|
expect(res.ok).toBe(false);
|
||||||
|
expect(res.error?.message ?? "").toContain("secure context");
|
||||||
ws.close();
|
ws.close();
|
||||||
await server.close();
|
await server.close();
|
||||||
restoreGatewayToken(prevToken);
|
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.gatewayControlUi = { allowInsecureAuth: true };
|
||||||
testState.gatewayAuth = { mode: "token", token: "secret" };
|
testState.gatewayAuth = { mode: "token", token: "secret" };
|
||||||
const { writeConfigFile } = await import("../config/config.js");
|
const { writeConfigFile } = await import("../config/config.js");
|
||||||
@@ -753,7 +772,8 @@ describe("gateway server auth/connect", () => {
|
|||||||
...CONTROL_UI_CLIENT,
|
...CONTROL_UI_CLIENT,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(res.ok).toBe(true);
|
expect(res.ok).toBe(false);
|
||||||
|
expect(res.error?.message ?? "").toContain("pairing required");
|
||||||
ws.close();
|
ws.close();
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -341,7 +341,9 @@ export function attachGatewayWsMessageHandler(params: {
|
|||||||
isControlUi && configSnapshot.gateway?.controlUi?.allowInsecureAuth === true;
|
isControlUi && configSnapshot.gateway?.controlUi?.allowInsecureAuth === true;
|
||||||
const disableControlUiDeviceAuth =
|
const disableControlUiDeviceAuth =
|
||||||
isControlUi && configSnapshot.gateway?.controlUi?.dangerouslyDisableDeviceAuth === true;
|
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 device = disableControlUiDeviceAuth ? null : deviceRaw;
|
||||||
|
|
||||||
const hasDeviceTokenCandidate = Boolean(connectParams.auth?.token && device);
|
const hasDeviceTokenCandidate = Boolean(connectParams.auth?.token && device);
|
||||||
@@ -428,7 +430,9 @@ export function attachGatewayWsMessageHandler(params: {
|
|||||||
|
|
||||||
if (isControlUi && !allowControlUiBypass) {
|
if (isControlUi && !allowControlUiBypass) {
|
||||||
const errorMessage = "control ui requires HTTPS or localhost (secure context)";
|
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);
|
sendHandshakeErrorResponse(ErrorCodes.INVALID_REQUEST, errorMessage);
|
||||||
close(1008, errorMessage);
|
close(1008, errorMessage);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -351,7 +351,7 @@ function collectGatewayConfigFindings(
|
|||||||
severity: "critical",
|
severity: "critical",
|
||||||
title: "Control UI allows insecure HTTP auth",
|
title: "Control UI allows insecure HTTP auth",
|
||||||
detail:
|
detail:
|
||||||
"gateway.controlUi.allowInsecureAuth=true allows token-only auth over HTTP and skips device identity.",
|
"gateway.controlUi.allowInsecureAuth=true is a legacy insecure-auth toggle; Control UI still enforces secure context and device identity unless dangerouslyDisableDeviceAuth is enabled.",
|
||||||
remediation: "Disable it or switch to HTTPS (Tailscale Serve) or localhost.",
|
remediation: "Disable it or switch to HTTPS (Tailscale Serve) or localhost.",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user