VYPR
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.

PackageAffected versionsPatched versions
openclawnpm
< 2026.4.22026.4.2

Affected products

1
  • cpe:2.3:a:openclaw:openclaw:*:*:*:*:*:node.js:*:*
    Range: <2026.4.2

Patches

1
676b748056b5

Limit connect snapshot metadata to admin-scoped clients (#58469)

https://github.com/openclaw/openclawAgustin RiveraApr 2, 2026via ghsa
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

News mentions

0

No linked articles in our index yet.