mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-19 11:28:38 +00:00
Gateway/ws: clear unbound scopes for shared-token auth (#44306)
* Gateway/ws: clear unbound shared-auth scopes * Gateway/auth: cover shared-token scope stripping * Changelog: add shared-token scope stripping entry * Gateway/ws: preserve allowed control-ui scopes * Gateway/auth: assert control-ui admin scopes survive allowed device-less auth * Gateway/auth: cover shared-password scope stripping
This commit is contained in:
@@ -35,6 +35,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
- Security/exec approvals: escape invisible Unicode format characters in approval prompts so zero-width command text renders as visible `\u{...}` escapes instead of spoofing the reviewed command. (`GHSA-pcqg-f7rg-xfvv`)(#43687) Thanks @EkiXu and @vincentkoc.
|
- Security/exec approvals: escape invisible Unicode format characters in approval prompts so zero-width command text renders as visible `\u{...}` escapes instead of spoofing the reviewed command. (`GHSA-pcqg-f7rg-xfvv`)(#43687) Thanks @EkiXu and @vincentkoc.
|
||||||
- Security/exec detection: normalize compatibility Unicode and strip invisible formatting code points before obfuscation checks so zero-width and fullwidth command tricks no longer suppress heuristic detection. (`GHSA-9r3v-37xh-2cf6`)(#44091) Thanks @wooluo and @vincentkoc.
|
- Security/exec detection: normalize compatibility Unicode and strip invisible formatting code points before obfuscation checks so zero-width and fullwidth command tricks no longer suppress heuristic detection. (`GHSA-9r3v-37xh-2cf6`)(#44091) Thanks @wooluo and @vincentkoc.
|
||||||
- Security/exec allowlist: preserve POSIX case sensitivity and keep `?` within a single path segment so exact-looking allowlist patterns no longer overmatch executables across case or directory boundaries. (`GHSA-f8r2-vg7x-gh8m`)(#43798) Thanks @zpbrent and @vincentkoc.
|
- Security/exec allowlist: preserve POSIX case sensitivity and keep `?` within a single path segment so exact-looking allowlist patterns no longer overmatch executables across case or directory boundaries. (`GHSA-f8r2-vg7x-gh8m`)(#43798) Thanks @zpbrent and @vincentkoc.
|
||||||
|
- Security/gateway auth: clear unbound client-declared scopes on shared-token WebSocket connects so device-less shared-token operators cannot self-declare elevated scopes. (`GHSA-rqpp-rjj8-7wv8`)(#44306) Thanks @LUOYEcode and @vincentkoc.
|
||||||
- Security/browser.request: block persistent browser profile create/delete routes from write-scoped `browser.request` so callers can no longer persist admin-only browser profile changes through the browser control surface. (`GHSA-vmhq-cqm9-6p7q`)(#43800) Thanks @tdjackey and @vincentkoc.
|
- Security/browser.request: block persistent browser profile create/delete routes from write-scoped `browser.request` so callers can no longer persist admin-only browser profile changes through the browser control surface. (`GHSA-vmhq-cqm9-6p7q`)(#43800) Thanks @tdjackey and @vincentkoc.
|
||||||
- Security/agent: reject public spawned-run lineage fields and keep workspace inheritance on the internal spawned-session path so external `agent` callers can no longer override the gateway workspace boundary. (`GHSA-2rqg-gjgv-84jm`)(#43801) Thanks @tdjackey and @vincentkoc.
|
- Security/agent: reject public spawned-run lineage fields and keep workspace inheritance on the internal spawned-session path so external `agent` callers can no longer override the gateway workspace boundary. (`GHSA-2rqg-gjgv-84jm`)(#43801) Thanks @tdjackey and @vincentkoc.
|
||||||
- Security/session_status: enforce sandbox session-tree visibility and shared agent-to-agent access guards before reading or mutating target session state, so sandboxed subagents can no longer inspect parent session metadata or write parent model overrides via `session_status`. (`GHSA-wcxr-59v9-rxr8`)(#43754) Thanks @tdjackey and @vincentkoc.
|
- Security/session_status: enforce sandbox session-tree visibility and shared agent-to-agent access guards before reading or mutating target session state, so sandboxed subagents can no longer inspect parent session metadata or write parent model overrides via `session_status`. (`GHSA-wcxr-59v9-rxr8`)(#43754) Thanks @tdjackey and @vincentkoc.
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
getFreePort,
|
getFreePort,
|
||||||
openWs,
|
openWs,
|
||||||
originForPort,
|
originForPort,
|
||||||
|
rpcReq,
|
||||||
restoreGatewayToken,
|
restoreGatewayToken,
|
||||||
startGatewayServer,
|
startGatewayServer,
|
||||||
testState,
|
testState,
|
||||||
@@ -62,6 +63,24 @@ describe("gateway auth compatibility baseline", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("clears client-declared scopes for shared-token operator connects", async () => {
|
||||||
|
const ws = await openWs(port);
|
||||||
|
try {
|
||||||
|
const res = await connectReq(ws, {
|
||||||
|
token: "secret",
|
||||||
|
scopes: ["operator.admin"],
|
||||||
|
device: null,
|
||||||
|
});
|
||||||
|
expect(res.ok).toBe(true);
|
||||||
|
|
||||||
|
const adminRes = await rpcReq(ws, "set-heartbeats", { enabled: false });
|
||||||
|
expect(adminRes.ok).toBe(false);
|
||||||
|
expect(adminRes.error?.message).toBe("missing scope: operator.admin");
|
||||||
|
} finally {
|
||||||
|
ws.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test("returns stable token-missing details for control ui without token", async () => {
|
test("returns stable token-missing details for control ui without token", async () => {
|
||||||
const ws = await openWs(port, { origin: originForPort(port) });
|
const ws = await openWs(port, { origin: originForPort(port) });
|
||||||
try {
|
try {
|
||||||
@@ -163,6 +182,24 @@ describe("gateway auth compatibility baseline", () => {
|
|||||||
ws.close();
|
ws.close();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("clears client-declared scopes for shared-password operator connects", async () => {
|
||||||
|
const ws = await openWs(port);
|
||||||
|
try {
|
||||||
|
const res = await connectReq(ws, {
|
||||||
|
password: "secret",
|
||||||
|
scopes: ["operator.admin"],
|
||||||
|
device: null,
|
||||||
|
});
|
||||||
|
expect(res.ok).toBe(true);
|
||||||
|
|
||||||
|
const adminRes = await rpcReq(ws, "set-heartbeats", { enabled: false });
|
||||||
|
expect(adminRes.ok).toBe(false);
|
||||||
|
expect(adminRes.error?.message).toBe("missing scope: operator.admin");
|
||||||
|
} finally {
|
||||||
|
ws.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("none mode", () => {
|
describe("none mode", () => {
|
||||||
|
|||||||
@@ -91,6 +91,11 @@ export function registerControlUiAndPairingSuite(): void {
|
|||||||
expect(health.ok).toBe(true);
|
expect(health.ok).toBe(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const expectAdminRpcOk = async (ws: WebSocket) => {
|
||||||
|
const admin = await rpcReq(ws, "set-heartbeats", { enabled: false });
|
||||||
|
expect(admin.ok).toBe(true);
|
||||||
|
};
|
||||||
|
|
||||||
const connectControlUiWithoutDeviceAndExpectOk = async (params: {
|
const connectControlUiWithoutDeviceAndExpectOk = async (params: {
|
||||||
ws: WebSocket;
|
ws: WebSocket;
|
||||||
token?: string;
|
token?: string;
|
||||||
@@ -104,6 +109,7 @@ export function registerControlUiAndPairingSuite(): void {
|
|||||||
});
|
});
|
||||||
expect(res.ok).toBe(true);
|
expect(res.ok).toBe(true);
|
||||||
await expectStatusAndHealthOk(params.ws);
|
await expectStatusAndHealthOk(params.ws);
|
||||||
|
await expectAdminRpcOk(params.ws);
|
||||||
};
|
};
|
||||||
|
|
||||||
const createOperatorIdentityFixture = async (identityPrefix: string) => {
|
const createOperatorIdentityFixture = async (identityPrefix: string) => {
|
||||||
@@ -217,6 +223,9 @@ export function registerControlUiAndPairingSuite(): void {
|
|||||||
}
|
}
|
||||||
if (tc.expectStatusChecks) {
|
if (tc.expectStatusChecks) {
|
||||||
await expectStatusAndHealthOk(ws);
|
await expectStatusAndHealthOk(ws);
|
||||||
|
if (tc.role === "operator") {
|
||||||
|
await expectAdminRpcOk(ws);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ws.close();
|
ws.close();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -643,15 +643,12 @@ export function attachGatewayWsMessageHandler(params: {
|
|||||||
close(1008, truncateCloseReason(authMessage));
|
close(1008, truncateCloseReason(authMessage));
|
||||||
};
|
};
|
||||||
const clearUnboundScopes = () => {
|
const clearUnboundScopes = () => {
|
||||||
if (scopes.length > 0 && !controlUiAuthPolicy.allowBypass && !sharedAuthOk) {
|
if (scopes.length > 0) {
|
||||||
scopes = [];
|
scopes = [];
|
||||||
connectParams.scopes = scopes;
|
connectParams.scopes = scopes;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const handleMissingDeviceIdentity = (): boolean => {
|
const handleMissingDeviceIdentity = (): boolean => {
|
||||||
if (!device) {
|
|
||||||
clearUnboundScopes();
|
|
||||||
}
|
|
||||||
const trustedProxyAuthOk = isTrustedProxyControlUiOperatorAuth({
|
const trustedProxyAuthOk = isTrustedProxyControlUiOperatorAuth({
|
||||||
isControlUi,
|
isControlUi,
|
||||||
role,
|
role,
|
||||||
@@ -670,6 +667,9 @@ export function attachGatewayWsMessageHandler(params: {
|
|||||||
hasSharedAuth,
|
hasSharedAuth,
|
||||||
isLocalClient,
|
isLocalClient,
|
||||||
});
|
});
|
||||||
|
if (!device && (!isControlUi || decision.kind !== "allow")) {
|
||||||
|
clearUnboundScopes();
|
||||||
|
}
|
||||||
if (decision.kind === "allow") {
|
if (decision.kind === "allow") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user