mirror of https://github.com/openclaw/openclaw.git
Limit connect snapshot metadata to admin-scoped clients (#58469)
* fix(gateway): gate connect snapshot metadata by scope * fix(gateway): clarify connect snapshot trust boundary * fix(gateway): note connect snapshot change in changelog * fix(gateway): remove changelog changes from PR * chore: add changelog for scoped gateway snapshot metadata --------- Co-authored-by: Devin Robison <drobison@nvidia.com>
This commit is contained in:
parent
a4a372825e
commit
676b748056
|
|
@ -23,6 +23,7 @@ Docs: https://docs.openclaw.ai
|
|||
- Exec/Windows: include Windows-compatible env override keys like `ProgramFiles(x86)` in system-run approval binding so changed approved values are rejected instead of silently passing unbound. (#59182) Thanks @pgondhi987.
|
||||
- ACP/Windows spawn: fail closed on unresolved `.cmd` and `.bat` OpenClaw wrappers unless a caller explicitly opts into shell fallback, so Windows ACP launches do not silently drop into shell-mediated execution when wrapper unwrapping fails. (#58436) Thanks @eleqtrizit.
|
||||
- Exec/Windows: prefer strict-inline-eval denial over generic allowlist prompts for interpreter carriers, while keeping persisted Windows allow-always approvals argv-bound. (#59780) Thanks @luoyanglang.
|
||||
- Gateway/connect: omit admin-scoped config and auth metadata from lower-privilege `hello-ok` snapshots while preserving those fields for admin reconnects. (#58469) Thanks @eleqtrizit.
|
||||
|
||||
## 2026.4.2-beta.1
|
||||
|
||||
|
|
|
|||
|
|
@ -277,6 +277,38 @@ describe("gateway auth browser hardening", () => {
|
|||
});
|
||||
});
|
||||
|
||||
test("omits sensitive gateway paths from low-privilege hello-ok snapshots", async () => {
|
||||
testState.gatewayAuth = { mode: "token", token: "secret" };
|
||||
await withGatewayServer(async ({ port }) => {
|
||||
const ws = await openWs(port, { origin: originForPort(port) });
|
||||
try {
|
||||
const payload = (await connectOk(ws, {
|
||||
token: "secret",
|
||||
scopes: ["operator.read"],
|
||||
device: null,
|
||||
})) as {
|
||||
type: "hello-ok";
|
||||
snapshot?: {
|
||||
configPath?: unknown;
|
||||
stateDir?: unknown;
|
||||
authMode?: unknown;
|
||||
};
|
||||
};
|
||||
// connectReq scopes are evaluated after auth and unbound-scope clearing, so this assertion
|
||||
// verifies the effective low-privilege session view rather than self-declared client scopes.
|
||||
const snapshot = payload.snapshot as
|
||||
| { configPath?: unknown; stateDir?: unknown; authMode?: unknown }
|
||||
| undefined;
|
||||
expect(snapshot).toBeDefined();
|
||||
expect(snapshot?.configPath).toBeUndefined();
|
||||
expect(snapshot?.stateDir).toBeUndefined();
|
||||
expect(snapshot?.authMode).toBeUndefined();
|
||||
} finally {
|
||||
ws.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("does not silently auto-pair non-control-ui browser clients on loopback", async () => {
|
||||
const { listDevicePairing } = await import("../infra/device-pairing.js");
|
||||
testState.gatewayAuth = { mode: "token", token: "secret" };
|
||||
|
|
|
|||
|
|
@ -190,7 +190,22 @@ describe("gateway auth compatibility baseline", () => {
|
|||
scopes: ["operator.admin"],
|
||||
});
|
||||
expect(res.ok).toBe(true);
|
||||
expect((res.payload as { type?: string } | undefined)?.type).toBe("hello-ok");
|
||||
const payload = res.payload as
|
||||
| {
|
||||
type?: string;
|
||||
snapshot?: {
|
||||
configPath?: string;
|
||||
stateDir?: string;
|
||||
authMode?: string;
|
||||
};
|
||||
}
|
||||
| undefined;
|
||||
expect(payload?.type).toBe("hello-ok");
|
||||
expect(typeof payload?.snapshot?.configPath).toBe("string");
|
||||
expect((payload?.snapshot?.configPath ?? "").length).toBeGreaterThan(0);
|
||||
expect(typeof payload?.snapshot?.stateDir).toBe("string");
|
||||
expect((payload?.snapshot?.stateDir ?? "").length).toBeGreaterThan(0);
|
||||
expect(payload?.snapshot?.authMode).toBe("token");
|
||||
} finally {
|
||||
ws.close();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,36 +14,38 @@ let healthCache: HealthSummary | null = null;
|
|||
let healthRefresh: Promise<HealthSummary> | null = null;
|
||||
let broadcastHealthUpdate: ((snap: HealthSummary) => void) | null = null;
|
||||
|
||||
export function buildGatewaySnapshot(): Snapshot {
|
||||
export function buildGatewaySnapshot(opts?: { includeSensitive?: boolean }): Snapshot {
|
||||
const cfg = loadConfig();
|
||||
const configPath = createConfigIO().configPath;
|
||||
const defaultAgentId = resolveDefaultAgentId(cfg);
|
||||
const mainKey = normalizeMainKey(cfg.session?.mainKey);
|
||||
const mainSessionKey = resolveMainSessionKey(cfg);
|
||||
const scope = cfg.session?.scope ?? "per-sender";
|
||||
const presence = listSystemPresence();
|
||||
const uptimeMs = Math.round(process.uptime() * 1000);
|
||||
const auth = resolveGatewayAuth({ authConfig: cfg.gateway?.auth, env: process.env });
|
||||
const updateAvailable = getUpdateAvailable() ?? undefined;
|
||||
// Health is async; caller should await getHealthSnapshot and replace later if needed.
|
||||
const emptyHealth: unknown = {};
|
||||
return {
|
||||
const snapshot: Snapshot = {
|
||||
presence,
|
||||
health: emptyHealth,
|
||||
stateVersion: { presence: presenceVersion, health: healthVersion },
|
||||
uptimeMs,
|
||||
// Surface resolved paths so UIs can display the true config location.
|
||||
configPath,
|
||||
stateDir: STATE_DIR,
|
||||
sessionDefaults: {
|
||||
defaultAgentId,
|
||||
mainKey,
|
||||
mainSessionKey,
|
||||
scope,
|
||||
},
|
||||
authMode: auth.mode,
|
||||
updateAvailable,
|
||||
};
|
||||
if (opts?.includeSensitive === true) {
|
||||
const auth = resolveGatewayAuth({ authConfig: cfg.gateway?.auth, env: process.env });
|
||||
// Surface resolved paths only to admin callers that already have broader gateway access.
|
||||
snapshot.configPath = createConfigIO().configPath;
|
||||
snapshot.stateDir = STATE_DIR;
|
||||
snapshot.authMode = auth.mode;
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
export function getHealthCache(): HealthSummary | null {
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ import {
|
|||
mintCanvasCapabilityToken,
|
||||
} from "../../canvas-capability.js";
|
||||
import { normalizeDeviceMetadataForAuth } from "../../device-auth.js";
|
||||
import { ADMIN_SCOPE } from "../../method-scopes.js";
|
||||
import {
|
||||
isLocalishHost,
|
||||
isLoopbackAddress,
|
||||
|
|
@ -1048,7 +1049,9 @@ export function attachGatewayWsMessageHandler(params: {
|
|||
incrementPresenceVersion();
|
||||
}
|
||||
|
||||
const snapshot = buildGatewaySnapshot();
|
||||
const snapshot = buildGatewaySnapshot({
|
||||
includeSensitive: scopes.includes(ADMIN_SCOPE),
|
||||
});
|
||||
const cachedHealth = getHealthCache();
|
||||
if (cachedHealth) {
|
||||
snapshot.health = cachedHealth;
|
||||
|
|
|
|||
Loading…
Reference in New Issue