mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 22:18:27 +00:00
fix(gateway): restore localhost Control UI pairing when allowInsecureAuth is set (#22996)
* fix(gateway): allow localhost Control UI without device identity when allowInsecureAuth is set * fix(gateway): pass isLocalClient to evaluateMissingDeviceIdentity * test: add regression tests for localhost Control UI pairing * fix(gateway): require pairing for legacy metadata upgrades * test(gateway): fix legacy metadata e2e ws typing --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -40,6 +40,7 @@ describe("ws connect policy", () => {
|
||||
sharedAuthOk: true,
|
||||
authOk: true,
|
||||
hasSharedAuth: true,
|
||||
isLocalClient: false,
|
||||
}).kind,
|
||||
).toBe("allow");
|
||||
|
||||
@@ -48,6 +49,7 @@ describe("ws connect policy", () => {
|
||||
controlUiConfig: { allowInsecureAuth: true, dangerouslyDisableDeviceAuth: false },
|
||||
deviceRaw: null,
|
||||
});
|
||||
// Remote Control UI with allowInsecureAuth -> still rejected.
|
||||
expect(
|
||||
evaluateMissingDeviceIdentity({
|
||||
hasDeviceIdentity: false,
|
||||
@@ -57,6 +59,40 @@ describe("ws connect policy", () => {
|
||||
sharedAuthOk: true,
|
||||
authOk: true,
|
||||
hasSharedAuth: true,
|
||||
isLocalClient: false,
|
||||
}).kind,
|
||||
).toBe("reject-control-ui-insecure-auth");
|
||||
|
||||
// Local Control UI with allowInsecureAuth -> allowed.
|
||||
expect(
|
||||
evaluateMissingDeviceIdentity({
|
||||
hasDeviceIdentity: false,
|
||||
role: "operator",
|
||||
isControlUi: true,
|
||||
controlUiAuthPolicy: controlUiStrict,
|
||||
sharedAuthOk: true,
|
||||
authOk: true,
|
||||
hasSharedAuth: true,
|
||||
isLocalClient: true,
|
||||
}).kind,
|
||||
).toBe("allow");
|
||||
|
||||
// Control UI without allowInsecureAuth, even on localhost -> rejected.
|
||||
const controlUiNoInsecure = resolveControlUiAuthPolicy({
|
||||
isControlUi: true,
|
||||
controlUiConfig: { dangerouslyDisableDeviceAuth: false },
|
||||
deviceRaw: null,
|
||||
});
|
||||
expect(
|
||||
evaluateMissingDeviceIdentity({
|
||||
hasDeviceIdentity: false,
|
||||
role: "operator",
|
||||
isControlUi: true,
|
||||
controlUiAuthPolicy: controlUiNoInsecure,
|
||||
sharedAuthOk: true,
|
||||
authOk: true,
|
||||
hasSharedAuth: true,
|
||||
isLocalClient: true,
|
||||
}).kind,
|
||||
).toBe("reject-control-ui-insecure-auth");
|
||||
|
||||
@@ -69,6 +105,7 @@ describe("ws connect policy", () => {
|
||||
sharedAuthOk: true,
|
||||
authOk: true,
|
||||
hasSharedAuth: true,
|
||||
isLocalClient: false,
|
||||
}).kind,
|
||||
).toBe("allow");
|
||||
|
||||
@@ -81,6 +118,7 @@ describe("ws connect policy", () => {
|
||||
sharedAuthOk: false,
|
||||
authOk: false,
|
||||
hasSharedAuth: true,
|
||||
isLocalClient: false,
|
||||
}).kind,
|
||||
).toBe("reject-unauthorized");
|
||||
|
||||
@@ -93,6 +131,7 @@ describe("ws connect policy", () => {
|
||||
sharedAuthOk: true,
|
||||
authOk: true,
|
||||
hasSharedAuth: true,
|
||||
isLocalClient: false,
|
||||
}).kind,
|
||||
).toBe("reject-device-required");
|
||||
});
|
||||
|
||||
@@ -53,12 +53,20 @@ export function evaluateMissingDeviceIdentity(params: {
|
||||
sharedAuthOk: boolean;
|
||||
authOk: boolean;
|
||||
hasSharedAuth: boolean;
|
||||
isLocalClient: boolean;
|
||||
}): MissingDeviceIdentityDecision {
|
||||
if (params.hasDeviceIdentity) {
|
||||
return { kind: "allow" };
|
||||
}
|
||||
if (params.isControlUi && !params.controlUiAuthPolicy.allowBypass) {
|
||||
return { kind: "reject-control-ui-insecure-auth" };
|
||||
// Allow localhost Control UI connections when allowInsecureAuth is configured.
|
||||
// Localhost has no network interception risk, and browser SubtleCrypto
|
||||
// (needed for device identity) is unavailable in insecure HTTP contexts.
|
||||
// Remote connections are still rejected to preserve the MitM protection
|
||||
// that the security fix (#20684) intended.
|
||||
if (!params.controlUiAuthPolicy.allowInsecureAuthConfigured || !params.isLocalClient) {
|
||||
return { kind: "reject-control-ui-insecure-auth" };
|
||||
}
|
||||
}
|
||||
if (roleCanSkipDeviceIdentity(params.role, params.sharedAuthOk)) {
|
||||
return { kind: "allow" };
|
||||
|
||||
@@ -469,6 +469,7 @@ export function attachGatewayWsMessageHandler(params: {
|
||||
sharedAuthOk,
|
||||
authOk,
|
||||
hasSharedAuth,
|
||||
isLocalClient,
|
||||
});
|
||||
if (decision.kind === "allow") {
|
||||
return true;
|
||||
@@ -706,50 +707,50 @@ export function attachGatewayWsMessageHandler(params: {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const hasLegacyPairedMetadata =
|
||||
paired.roles === undefined && paired.scopes === undefined;
|
||||
const pairedRoles = Array.isArray(paired.roles)
|
||||
? paired.roles
|
||||
: paired.role
|
||||
? [paired.role]
|
||||
: [];
|
||||
if (!hasLegacyPairedMetadata) {
|
||||
const allowedRoles = new Set(pairedRoles);
|
||||
if (allowedRoles.size === 0) {
|
||||
logUpgradeAudit("role-upgrade", pairedRoles, paired.scopes);
|
||||
const ok = await requirePairing("role-upgrade");
|
||||
if (!ok) {
|
||||
return;
|
||||
}
|
||||
} else if (!allowedRoles.has(role)) {
|
||||
logUpgradeAudit("role-upgrade", pairedRoles, paired.scopes);
|
||||
const ok = await requirePairing("role-upgrade");
|
||||
if (!ok) {
|
||||
return;
|
||||
}
|
||||
const pairedScopes = Array.isArray(paired.scopes)
|
||||
? paired.scopes
|
||||
: Array.isArray(paired.approvedScopes)
|
||||
? paired.approvedScopes
|
||||
: [];
|
||||
const allowedRoles = new Set(pairedRoles);
|
||||
if (allowedRoles.size === 0) {
|
||||
logUpgradeAudit("role-upgrade", pairedRoles, pairedScopes);
|
||||
const ok = await requirePairing("role-upgrade");
|
||||
if (!ok) {
|
||||
return;
|
||||
}
|
||||
} else if (!allowedRoles.has(role)) {
|
||||
logUpgradeAudit("role-upgrade", pairedRoles, pairedScopes);
|
||||
const ok = await requirePairing("role-upgrade");
|
||||
if (!ok) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const pairedScopes = Array.isArray(paired.scopes) ? paired.scopes : [];
|
||||
if (scopes.length > 0) {
|
||||
if (pairedScopes.length === 0) {
|
||||
if (scopes.length > 0) {
|
||||
if (pairedScopes.length === 0) {
|
||||
logUpgradeAudit("scope-upgrade", pairedRoles, pairedScopes);
|
||||
const ok = await requirePairing("scope-upgrade");
|
||||
if (!ok) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const scopesAllowed = roleScopesAllow({
|
||||
role,
|
||||
requestedScopes: scopes,
|
||||
allowedScopes: pairedScopes,
|
||||
});
|
||||
if (!scopesAllowed) {
|
||||
logUpgradeAudit("scope-upgrade", pairedRoles, pairedScopes);
|
||||
const ok = await requirePairing("scope-upgrade");
|
||||
if (!ok) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const scopesAllowed = roleScopesAllow({
|
||||
role,
|
||||
requestedScopes: scopes,
|
||||
allowedScopes: pairedScopes,
|
||||
});
|
||||
if (!scopesAllowed) {
|
||||
logUpgradeAudit("scope-upgrade", pairedRoles, pairedScopes);
|
||||
const ok = await requirePairing("scope-upgrade");
|
||||
if (!ok) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user