VYPR
High severity7.3NVD Advisory· Published Apr 23, 2026· Updated Apr 29, 2026

CVE-2026-41342

CVE-2026-41342

Description

OpenClaw before 2026.3.28 contains an authentication bypass vulnerability in the remote onboarding component that persists unauthenticated discovery endpoints without explicit trust confirmation. Attackers can spoof discovery endpoints to redirect onboarding toward malicious gateways and capture gateway credentials or traffic.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
openclawnpm
< 2026.3.282026.3.28

Affected products

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

Patches

1
d6affb17d85f

CLI: confirm discovered remote gateways before saving config (#55895)

https://github.com/openclaw/openclawJacob TomlinsonMar 27, 2026via ghsa
2 files changed · +122 0
  • src/commands/onboard-remote.test.ts+104 0 modified
    @@ -83,6 +83,7 @@ describe("promptRemoteGatewayConfig", () => {
             displayName: "Gateway",
             host: "gateway.tailnet.ts.net",
             port: 18789,
    +        gatewayTlsFingerprintSha256: "sha256:abc123",
           },
         ]);
     
    @@ -111,12 +112,115 @@ describe("promptRemoteGatewayConfig", () => {
         expect(next.gateway?.mode).toBe("remote");
         expect(next.gateway?.remote?.url).toBe("wss://gateway.tailnet.ts.net:18789");
         expect(next.gateway?.remote?.token).toBe("token-123");
    +    expect(next.gateway?.remote?.tlsFingerprint).toBe("sha256:abc123");
         expect(prompter.note).toHaveBeenCalledWith(
           expect.stringContaining("Direct remote access defaults to TLS."),
           "Direct remote",
         );
       });
     
    +  it("rejects discovery endpoint when trust confirmation is declined", async () => {
    +    detectBinary.mockResolvedValue(true);
    +    discoverGatewayBeacons.mockResolvedValue([
    +      {
    +        instanceName: "evil",
    +        displayName: "Evil",
    +        host: "evil.example",
    +        port: 443,
    +        gatewayTlsFingerprintSha256: "sha256:attacker",
    +      },
    +    ]);
    +
    +    const select = createSelectPrompter({
    +      "Select gateway": "0",
    +      "Connection method": "direct",
    +    });
    +    const confirm: WizardPrompter["confirm"] = vi.fn(async (params) => {
    +      if (params.message.startsWith("Discover gateway")) {
    +        return true;
    +      }
    +      if (params.message.startsWith("Trust this gateway")) {
    +        return false;
    +      }
    +      return false;
    +    });
    +
    +    const prompter = createPrompter({
    +      confirm,
    +      select,
    +      text: vi.fn(async () => "") as WizardPrompter["text"],
    +    });
    +
    +    await expect(promptRemoteGatewayConfig({} as OpenClawConfig, prompter)).rejects.toThrow(
    +      "not trusted",
    +    );
    +  });
    +
    +  it("trusts discovery endpoint without fingerprint and omits tlsFingerprint", async () => {
    +    detectBinary.mockResolvedValue(true);
    +    discoverGatewayBeacons.mockResolvedValue([
    +      {
    +        instanceName: "gw",
    +        displayName: "Gateway",
    +        host: "gw.example",
    +        port: 18789,
    +      },
    +    ]);
    +
    +    const text: WizardPrompter["text"] = vi.fn(async (params) => {
    +      if (params.message === "Gateway WebSocket URL") {
    +        return String(params.initialValue);
    +      }
    +      return "";
    +    }) as WizardPrompter["text"];
    +
    +    const { next } = await runRemotePrompt({
    +      text,
    +      confirm: true,
    +      selectResponses: {
    +        "Select gateway": "0",
    +        "Connection method": "direct",
    +        "Gateway auth": "off",
    +      },
    +    });
    +
    +    expect(next.gateway?.remote?.url).toBe("wss://gw.example:18789");
    +    expect(next.gateway?.remote?.tlsFingerprint).toBeUndefined();
    +  });
    +
    +  it("drops discovery tlsFingerprint when the URL is edited after trust confirmation", async () => {
    +    detectBinary.mockResolvedValue(true);
    +    discoverGatewayBeacons.mockResolvedValue([
    +      {
    +        instanceName: "gateway",
    +        displayName: "Gateway",
    +        host: "gateway.tailnet.ts.net",
    +        port: 18789,
    +        gatewayTlsFingerprintSha256: "sha256:abc123",
    +      },
    +    ]);
    +
    +    const text: WizardPrompter["text"] = vi.fn(async (params) => {
    +      if (params.message === "Gateway WebSocket URL") {
    +        return "wss://other.example:443";
    +      }
    +      return "";
    +    }) as WizardPrompter["text"];
    +
    +    const { next } = await runRemotePrompt({
    +      text,
    +      confirm: true,
    +      selectResponses: {
    +        "Select gateway": "0",
    +        "Connection method": "direct",
    +        "Gateway auth": "off",
    +      },
    +    });
    +
    +    expect(next.gateway?.remote?.url).toBe("wss://other.example:443");
    +    expect(next.gateway?.remote?.tlsFingerprint).toBeUndefined();
    +  });
    +
       it("does not route from TXT-only discovery metadata", async () => {
         detectBinary.mockResolvedValue(true);
         discoverGatewayBeacons.mockResolvedValue([
    
  • src/commands/onboard-remote.ts+18 0 modified
    @@ -52,6 +52,8 @@ export async function promptRemoteGatewayConfig(
     ): Promise<OpenClawConfig> {
       let selectedBeacon: GatewayBonjourBeacon | null = null;
       let suggestedUrl = cfg.gateway?.remote?.url ?? DEFAULT_GATEWAY_URL;
    +  let discoveryTlsFingerprint: string | undefined;
    +  let trustedDiscoveryUrl: string | undefined;
     
       const hasBonjourTool = (await detectBinary("dns-sd")) || (await detectBinary("avahi-browse"));
       const wantsDiscover = hasBonjourTool
    @@ -113,10 +115,23 @@ export async function promptRemoteGatewayConfig(
           });
           if (mode === "direct") {
             suggestedUrl = `wss://${host}:${port}`;
    +        const fingerprint = target.endpoint.gatewayTlsFingerprintSha256;
    +        const trusted = await prompter.confirm({
    +          message: `Trust this gateway? Host: ${host}:${port} TLS fingerprint: ${fingerprint ?? "not advertised (connection will not be pinned)"}`,
    +          initialValue: false,
    +        });
    +        if (!trusted) {
    +          throw new Error(
    +            `Discovery endpoint ${host}:${port} not trusted. Re-run onboarding or enter the URL manually.`,
    +          );
    +        }
    +        discoveryTlsFingerprint = fingerprint;
    +        trustedDiscoveryUrl = suggestedUrl;
             await prompter.note(
               [
                 "Direct remote access defaults to TLS.",
                 `Using: ${suggestedUrl}`,
    +            ...(fingerprint ? [`TLS pin: ${fingerprint}`] : []),
                 "If your gateway is loopback-only, choose SSH tunnel and keep ws://127.0.0.1:18789.",
               ].join("\n"),
               "Direct remote",
    @@ -141,6 +156,8 @@ export async function promptRemoteGatewayConfig(
         validate: (value) => validateGatewayWebSocketUrl(String(value)),
       });
       const url = ensureWsUrl(String(urlInput));
    +  const pinnedDiscoveryFingerprint =
    +    discoveryTlsFingerprint && url === trustedDiscoveryUrl ? discoveryTlsFingerprint : undefined;
     
       const authChoice = await prompter.select({
         message: "Gateway auth",
    @@ -231,6 +248,7 @@ export async function promptRemoteGatewayConfig(
             url,
             ...(token !== undefined ? { token } : {}),
             ...(password !== undefined ? { password } : {}),
    +        ...(pinnedDiscoveryFingerprint ? { tlsFingerprint: pinnedDiscoveryFingerprint } : {}),
           },
         },
       };
    

Vulnerability mechanics

AI mechanics synthesis has not run for this CVE yet.

References

5

News mentions

0

No linked articles in our index yet.