Medium severity4.3NVD Advisory· Published Apr 23, 2026· Updated Apr 29, 2026
CVE-2026-41339
CVE-2026-41339
Description
OpenClaw before 2026.4.2 exposes configPath and stateDir metadata in Gateway connect success snapshots to non-admin authenticated clients. Non-admin clients can recover host-specific filesystem paths and deployment details, enabling host fingerprinting and facilitating chained attacks.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
openclawnpm | < 2026.4.2 | 2026.4.2 |
Affected products
1Patches
1676b748056b5Limit connect snapshot metadata to admin-scoped clients (#58469)
5 files changed · +63 −10
CHANGELOG.md+1 −0 modified@@ -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
src/gateway/server.auth.browser-hardening.test.ts+32 −0 modified@@ -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" };
src/gateway/server.auth.compat-baseline.test.ts+16 −1 modified@@ -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(); }
src/gateway/server/health-state.ts+10 −8 modified@@ -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 {
src/gateway/server/ws-connection/message-handler.ts+4 −1 modified@@ -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;
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/openclaw/openclaw/commit/676b748056b5efca6f1255708e9dd9469edf5e2envdPatchWEB
- github.com/advisories/GHSA-2f7j-rp58-mr42ghsaADVISORY
- github.com/openclaw/openclaw/security/advisories/GHSA-2f7j-rp58-mr42nvdVendor AdvisoryWEB
- nvd.nist.gov/vuln/detail/CVE-2026-41339ghsaADVISORY
- www.vulncheck.com/advisories/openclaw-information-disclosure-via-gateway-connect-snapshotnvdThird Party AdvisoryWEB
News mentions
0No linked articles in our index yet.