VYPR
Medium severity5.8NVD Advisory· Published Apr 28, 2026· Updated Apr 28, 2026

CVE-2026-41372

CVE-2026-41372

Description

OpenClaw before 2026.4.2 fails to normalize trailing-dot localhost hosts in remote CDP discovery responses, allowing bypass of loopback protections. Attackers can craft hostile discovery responses returning localhost. to retarget authenticated browser control toward localhost endpoints and expose browser state.

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
9c22d6366973

Browser: normalize localhost absolute-form CDP hosts (#59236)

https://github.com/openclaw/openclawmappel-nvApr 2, 2026via ghsa
4 files changed · +22 2
  • CHANGELOG.md+1 0 modified
    @@ -53,6 +53,7 @@ Docs: https://docs.openclaw.ai
     - ACP/gateway reconnects: reject stale pre-ack ACP prompts after reconnect grace expiry so callers fail cleanly instead of hanging indefinitely when the gateway never confirms the run.
     - Providers/Copilot: classify native GitHub Copilot API hosts in the shared provider endpoint resolver and harden token-derived proxy endpoint parsing so Copilot base URL routing stays centralized and fails closed on malformed hints. Thanks @vincentkoc.
     - Gateway: prune empty `node-pending-work` state entries after explicit acknowledgments and natural expiry so the per-node state map no longer grows indefinitely. (#58179) Thanks @gavyngong.
    +- Browser/CDP: normalize trailing-dot localhost absolute-form hosts before loopback checks so remote CDP websocket URLs like `ws://localhost.:...` rewrite back to the configured remote host. (#59236) Thanks @mappel-nv.
     
     ## 2026.4.1-beta.1
     
    
  • extensions/browser/src/browser/cdp.test.ts+8 0 modified
    @@ -320,6 +320,14 @@ describe("cdp", () => {
         expect(normalized).toBe("wss://user:pass@example.com/devtools/browser/ABC?token=abc");
       });
     
    +  it("rewrites localhost absolute-form websocket URLs for remote CDP hosts", () => {
    +    const normalized = normalizeCdpWsUrl(
    +      "ws://localhost.:9222/devtools/browser/ABC",
    +      "https://user:pass@example.com?token=abc",
    +    );
    +    expect(normalized).toBe("wss://user:pass@example.com/devtools/browser/ABC?token=abc");
    +  });
    +
       it("rewrites 0.0.0.0 wildcard bind address to remote CDP host", () => {
         const normalized = normalizeCdpWsUrl(
           "ws://0.0.0.0:3000/devtools/browser/ABC",
    
  • src/gateway/net.test.ts+10 0 modified
    @@ -3,6 +3,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
     import { makeNetworkInterfacesSnapshot } from "../test-helpers/network-interfaces.js";
     import {
       isLocalishHost,
    +  isLoopbackHost,
       isPrivateOrLoopbackAddress,
       isPrivateOrLoopbackHost,
       isSecureWebSocketUrl,
    @@ -28,6 +29,7 @@ describe("isLocalishHost", () => {
       it("accepts loopback and tailscale serve/funnel host headers", () => {
         const accepted = [
           "localhost",
    +      "localhost.:18789",
           "127.0.0.1:18789",
           "[::1]:18789",
           "[::ffff:127.0.0.1]:18789",
    @@ -46,6 +48,13 @@ describe("isLocalishHost", () => {
       });
     });
     
    +describe("isLoopbackHost", () => {
    +  it("accepts localhost absolute-form hostnames", () => {
    +    expect(isLoopbackHost("localhost.")).toBe(true);
    +    expect(isLoopbackHost("LOCALHOST...")).toBe(true);
    +  });
    +});
    +
     describe("isTrustedProxyAddress", () => {
       it.each([
         {
    @@ -394,6 +403,7 @@ describe("isPrivateOrLoopbackAddress", () => {
     describe("isPrivateOrLoopbackHost", () => {
       it("accepts localhost", () => {
         expect(isPrivateOrLoopbackHost("localhost")).toBe(true);
    +    expect(isPrivateOrLoopbackHost("localhost.")).toBe(true);
       });
     
       it("accepts loopback addresses", () => {
    
  • src/gateway/net.ts+3 2 modified
    @@ -400,8 +400,9 @@ function parseHostForAddressChecks(
         return null;
       }
       const normalizedHost = host.trim().toLowerCase();
    -  if (normalizedHost === "localhost") {
    -    return { isLocalhost: true, unbracketedHost: normalizedHost };
    +  const canonicalHost = normalizedHost.replace(/\.+$/, "");
    +  if (canonicalHost === "localhost") {
    +    return { isLocalhost: true, unbracketedHost: canonicalHost };
       }
       return {
         isLocalhost: false,
    

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.