VYPR
Medium severity5.3NVD Advisory· Published Jun 15, 2026

CVE-2026-9595

CVE-2026-9595

Description

webpack-dev-server proxy with broad context and ws:true leaks HMR WebSocket to backend, leaking cookies and bypassing Host checks.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

webpack-dev-server proxy with broad context and ws:true leaks HMR WebSocket to backend, leaking cookies and bypassing Host checks.

Vulnerability

The vulnerability resides in webpack-dev-server prior to version 5.2.5. When a user configures a proxy entry with a broad context (e.g., /) and sets ws: true, the proxy intercepts not only intended backend WebSocket traffic but also the development server's own Hot Module Replacement (HMR) WebSocket connection. This occurs because the dev server's HMR WebSocket endpoint (typically under /sockjs-node or a similar path) matches the broadly defined proxy context, causing the proxy to forward these internal connections to the configured proxy target. Affected versions include all webpack-dev-server releases before 5.2.5 [1][4].

Exploitation

An attacker must be in a position to control the proxy target that the developer configures (e.g., a malicious or compromised backend service) or the developer must inadvertently point the proxy at an attacker-controlled server. The attacker does not require direct network access to the dev server; instead, the malicious proxy target receives the forwarded HMR WebSocket connection. When the HMR WebSocket is forwarded, the request carries the browser's cookies and Origin header from the development session. The dev server’s Host/Origin validation is bypassed because the connection is diverted before validation. Furthermore, the proxy target can send data back on the same socket, corrupting the HMR communication and potentially executing arbitrary JavaScript payloads in the developer's browser [4].

Impact

On successful exploitation, the attacker can leak sensitive cookies and the Origin header from the developer's browsing session. Additionally, because the HMR socket is shared and writable by both the legitimate HMR process and the proxy target, the attacker can inject arbitrary JavaScript into the page via HMR messages, leading to cross-origin information disclosure, session hijacking, or even remote code execution within the scope of the development site. The impact is confined to developer machines using the vulnerable configuration during development [4].

Mitigation

The issue is fixed in webpack-dev-server version 5.2.5, released on 2026-06-15 [1][4]. Users should upgrade to this version or later. For those unable to upgrade, workarounds include: scoping user-defined proxy context to specific paths (e.g., /api) instead of /, or omitting ws: true from the proxy entry when WebSocket forwarding is not required [4]. Prior workarounds in vue-cli and create-react-app have also been implemented to exclude the HMR socket path from proxying [2][3].

AI Insight generated on Jun 15, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

2

Patches

3
72ba7505aff2

fix: should not proxy sockjs endpoint (#4550)

https://github.com/vuejs/vue-cliHaoqun JiangSep 7, 2019via nvd-ref
1 file changed · +6 2
  • packages/@vue/cli-service/lib/util/prepareProxy.js+6 2 modified
    @@ -44,10 +44,14 @@ module.exports = function prepareProxy (proxy, appPublicFolder) {
         process.exit(1)
       }
     
    -  // Otherwise, if proxy is specified, we will let it handle any request except for files in the public folder.
    +  // If proxy is specified, let it handle any request except for
    +  // files in the public folder and requests to the WebpackDevServer socket endpoint.
    +  // https://github.com/facebook/create-react-app/issues/6720
       function mayProxy (pathname) {
         const maybePublicPath = path.resolve(appPublicFolder, pathname.slice(1))
    -    return !fs.existsSync(maybePublicPath)
    +    const isPublicFileRequest = fs.existsSync(maybePublicPath)
    +    const isWdsEndpointRequest = pathname.startsWith('/sockjs-node') // used by webpackHotDevClient
    +    return !(isPublicFileRequest || isWdsEndpointRequest)
       }
     
       function createProxyEntry (target, usersOnProxyReq, context) {
    
93e899612433

fix: skip HMR websocket path when forwarding upgrades to user-defined proxies (#4316)

https://github.com/webpack/webpack-dev-serverAndrew HyndmanMay 25, 2026via body-scan-shorthand
6 files changed · +233 15
  • lib/Server.js+23 5 modified
    @@ -1850,13 +1850,31 @@ class Server {
           /** @type {RequestHandler[]} */
           (this.webSocketProxies);
     
    +    const hmrPath =
    +      this.options.webSocketServer &&
    +      /** @type {WebSocketServerConfiguration} */
    +      (this.options.webSocketServer).options &&
    +      /** @type {NonNullable<WebSocketServerConfiguration["options"]>} */
    +      (
    +        /** @type {WebSocketServerConfiguration} */
    +        (this.options.webSocketServer).options
    +      ).path;
    +
         for (const webSocketProxy of webSocketProxies) {
    -      /** @type {S} */
    -      (this.server).on(
    -        "upgrade",
    +      const proxyUpgrade =
             /** @type {RequestHandler & { upgrade: NonNullable<RequestHandler["upgrade"]> }} */
    -        (webSocketProxy).upgrade,
    -      );
    +        (webSocketProxy).upgrade;
    +
    +      /** @type {S} */
    +      (this.server).on("upgrade", (req, socket, head) => {
    +        if (hmrPath && req.url) {
    +          const { pathname } = new URL(req.url, "http://0.0.0.0");
    +          if (pathname === hmrPath) {
    +            return;
    +          }
    +        }
    +        proxyUpgrade(req, socket, head);
    +      });
         }
       }
     
    
  • test/e2e/__snapshots__/api.test.js.snap.webpack5+2 2 modified
    @@ -29,7 +29,7 @@ exports[`API Server.checkHostHeader should allow URLs with scheme for checking o
       "[webpack-dev-server] Server started: Hot Module Replacement enabled, Live Reloading enabled, Progress disabled, Overlay enabled.",
       "[HMR] Waiting for update signal from WDS...",
       "Hey.",
    -  "WebSocket connection to 'ws://test.host:8158/ws' failed: Error in connection establishment: net::ERR_NAME_NOT_RESOLVED",
    +  "WebSocket connection to 'ws://test.host:8159/ws' failed: Error in connection establishment: net::ERR_NAME_NOT_RESOLVED",
       "[webpack-dev-server] JSHandle@object",
       "[webpack-dev-server] Disconnected!",
       "[webpack-dev-server] Trying to reconnect...",
    @@ -40,7 +40,7 @@ exports[`API Server.checkHostHeader should allow URLs with scheme for checking o
     
     exports[`API Server.checkHostHeader should allow URLs with scheme for checking origin when the "option.client.webSocketURL" is object: response status 1`] = `200`;
     
    -exports[`API Server.checkHostHeader should allow URLs with scheme for checking origin when the "option.client.webSocketURL" is object: web socket URL 1`] = `"ws://test.host:8158/ws"`;
    +exports[`API Server.checkHostHeader should allow URLs with scheme for checking origin when the "option.client.webSocketURL" is object: web socket URL 1`] = `"ws://test.host:8159/ws"`;
     
     exports[`API Server.getFreePort should retry finding the port for up to defaultPortRetry times (number): console messages 1`] = `
     [
    
  • test/e2e/__snapshots__/client-reconnect.test.js.snap.webpack5+2 2 modified
    @@ -20,10 +20,10 @@ exports[`client.reconnect option specified as number should try to reconnect 2 t
       "Hey.",
       "[webpack-dev-server] Disconnected!",
       "[webpack-dev-server] Trying to reconnect...",
    -  "WebSocket connection to 'ws://localhost:8163/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED",
    +  "WebSocket connection to 'ws://localhost:8164/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED",
       "[webpack-dev-server] JSHandle@object",
       "[webpack-dev-server] Trying to reconnect...",
    -  "WebSocket connection to 'ws://localhost:8163/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED",
    +  "WebSocket connection to 'ws://localhost:8164/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED",
       "[webpack-dev-server] JSHandle@object",
     ]
     `;
    
  • test/e2e/__snapshots__/port.test.js.snap.webpack5+4 4 modified
    @@ -20,25 +20,25 @@ exports[`port should work using "0" port : console messages 1`] = `
     
     exports[`port should work using "0" port : page errors 1`] = `[]`;
     
    -exports[`port should work using "8161" port : console messages 1`] = `
    +exports[`port should work using "8162" port : console messages 1`] = `
     [
       "[webpack-dev-server] Server started: Hot Module Replacement enabled, Live Reloading enabled, Progress disabled, Overlay enabled.",
       "[HMR] Waiting for update signal from WDS...",
       "Hey.",
     ]
     `;
     
    -exports[`port should work using "8161" port : console messages 2`] = `
    +exports[`port should work using "8162" port : console messages 2`] = `
     [
       "[webpack-dev-server] Server started: Hot Module Replacement enabled, Live Reloading enabled, Progress disabled, Overlay enabled.",
       "[HMR] Waiting for update signal from WDS...",
       "Hey.",
     ]
     `;
     
    -exports[`port should work using "8161" port : page errors 1`] = `[]`;
    +exports[`port should work using "8162" port : page errors 1`] = `[]`;
     
    -exports[`port should work using "8161" port : page errors 2`] = `[]`;
    +exports[`port should work using "8162" port : page errors 2`] = `[]`;
     
     exports[`port should work using "auto" port : console messages 1`] = `
     [
    
  • test/ports-map.js+1 1 modified
    @@ -35,7 +35,7 @@ const listOfTests = {
       "on-listening-option": 1,
       "open-option": 1,
       "port-option": 1,
    -  "proxy-option": 4,
    +  "proxy-option": 5,
       server: 1,
       "setup-exit-signals-option": 1,
       "static-directory-option": 1,
    
  • test/server/proxy-option.test.js+201 1 modified
    @@ -1,5 +1,6 @@
     "use strict";
     
    +const http = require("node:http");
     const path = require("node:path");
     const util = require("node:util");
     const express = require("express");
    @@ -8,7 +9,8 @@ const webpack = require("webpack");
     const WebSocket = require("ws");
     const Server = require("../../lib/Server");
     const config = require("../fixtures/proxy-config/webpack.config");
    -const [port1, port2, port3, port4] = require("../ports-map")["proxy-option"];
    +const [port1, port2, port3, port4, port5] =
    +  require("../ports-map")["proxy-option"];
     
     const WebSocketServer = WebSocket.Server;
     const staticDirectory = path.resolve(__dirname, "../fixtures/proxy-config");
    @@ -659,6 +661,204 @@ describe("proxy option", () => {
         }
       });
     
    +  describe("should not silently proxy dev-server HMR websocket to a permissive backend", () => {
    +    let server;
    +    let backend;
    +    let backendWss;
    +    let backendUpgradeCount;
    +
    +    const BACKEND_MESSAGE_TYPE = "backend-message";
    +
    +    beforeAll(async () => {
    +      backendUpgradeCount = 0;
    +
    +      backend = http.createServer();
    +      backendWss = new WebSocketServer({ server: backend });
    +      backendWss.on("connection", (connection) => {
    +        backendUpgradeCount += 1;
    +        connection.send(JSON.stringify({ type: BACKEND_MESSAGE_TYPE }));
    +      });
    +
    +      await new Promise((resolve) => {
    +        backend.listen(port5, resolve);
    +      });
    +
    +      const compiler = webpack(config);
    +
    +      server = new Server(
    +        {
    +          hot: true,
    +          allowedHosts: "all",
    +          webSocketServer: "ws",
    +          proxy: [
    +            {
    +              context: "/",
    +              target: `http://localhost:${port5}`,
    +              ws: true,
    +            },
    +          ],
    +          port: port3,
    +        },
    +        compiler,
    +      );
    +
    +      await server.start();
    +    });
    +
    +    afterAll(async () => {
    +      for (const client of backendWss.clients) {
    +        client.terminate();
    +      }
    +      backendWss.close();
    +      // Force-drop any lingering proxy-opened sockets so backend.close() does
    +      // not hang when the fix is missing and the proxy is mid-upgrade.
    +      backend.closeAllConnections();
    +      await server.stop();
    +      await new Promise((resolve) => {
    +        backend.close(resolve);
    +      });
    +    });
    +
    +    it("delivers the HMR control messages and never reaches the proxy target", async () => {
    +      const messages = [];
    +
    +      const ws = new WebSocket(`ws://localhost:${port3}/ws`);
    +
    +      await new Promise((resolve, reject) => {
    +        const timer = setTimeout(() => {
    +          reject(
    +            new Error(
    +              `Timed out waiting for HMR message. Got: ${JSON.stringify(messages)}`,
    +            ),
    +          );
    +        }, 3000);
    +
    +        ws.on("message", (raw) => {
    +          const parsed = JSON.parse(raw.toString());
    +          messages.push(parsed);
    +          if (parsed.type === "hot") {
    +            clearTimeout(timer);
    +            resolve();
    +          }
    +        });
    +
    +        ws.on("error", (err) => {
    +          clearTimeout(timer);
    +          reject(err);
    +        });
    +      });
    +
    +      ws.close();
    +
    +      // Let the proxy finish its async forwarding so the assertion below sees
    +      // the upgrade attempt deterministically.
    +      await new Promise((resolve) => {
    +        setTimeout(resolve, 300);
    +      });
    +
    +      expect(messages.some((m) => m.type === "hot")).toBe(true);
    +      expect(messages.some((m) => m.type === BACKEND_MESSAGE_TYPE)).toBe(false);
    +      expect(backendUpgradeCount).toBe(0);
    +    });
    +  });
    +
    +  describe("should not log proxy errors for the dev-server HMR upgrade", () => {
    +    let server;
    +    let backend;
    +    let stderrSpy;
    +
    +    beforeAll(async () => {
    +      stderrSpy = jest
    +        .spyOn(process.stderr, "write")
    +        .mockImplementation(() => true);
    +
    +      backend = http.createServer();
    +      backend.on("upgrade", (req, socket) => {
    +        socket.destroy();
    +      });
    +      await new Promise((resolve) => {
    +        backend.listen(port5, resolve);
    +      });
    +
    +      const compiler = webpack(config);
    +
    +      server = new Server(
    +        {
    +          hot: true,
    +          allowedHosts: "all",
    +          webSocketServer: "ws",
    +          proxy: [
    +            {
    +              context: "/",
    +              target: `http://localhost:${port5}`,
    +              ws: true,
    +            },
    +          ],
    +          port: port3,
    +        },
    +        compiler,
    +      );
    +
    +      await server.start();
    +    });
    +
    +    afterAll(async () => {
    +      stderrSpy.mockRestore();
    +      backend.closeAllConnections();
    +      await server.stop();
    +      await new Promise((resolve) => {
    +        backend.close(resolve);
    +      });
    +    });
    +
    +    it("does not surface any [HPM] error when the HMR client connects", async () => {
    +      const messages = [];
    +
    +      const ws = new WebSocket(`ws://localhost:${port3}/ws`);
    +
    +      await new Promise((resolve, reject) => {
    +        const timer = setTimeout(() => {
    +          reject(
    +            new Error(
    +              `Timed out waiting for HMR message. Got: ${JSON.stringify(messages)}`,
    +            ),
    +          );
    +        }, 3000);
    +
    +        ws.on("message", (raw) => {
    +          const parsed = JSON.parse(raw.toString());
    +          messages.push(parsed);
    +          if (parsed.type === "hot") {
    +            clearTimeout(timer);
    +            resolve();
    +          }
    +        });
    +
    +        ws.on("error", (err) => {
    +          clearTimeout(timer);
    +          reject(err);
    +        });
    +      });
    +
    +      ws.close();
    +
    +      await new Promise((resolve) => {
    +        setTimeout(resolve, 200);
    +      });
    +
    +      const hpmLines = stderrSpy.mock.calls
    +        .map((c) => c[0])
    +        .join("")
    +        .split("\n")
    +        .filter((line) => line.includes("[HPM]"))
    +        .map((line) => line.replaceAll(/localhost:\d+/g, "localhost:<port>"))
    +        .join("\n");
    +
    +      expect(hpmLines).toBe("");
    +      expect(messages.some((m) => m.type === "hot")).toBe(true);
    +    });
    +  });
    +
       describe("should supports http methods", () => {
         let server;
         let req;
    
eaf0b176e2d2

Fix HMR in Firefox when proxy option present (#7444)

https://github.com/facebook/create-react-appDmitry LepskiyAug 7, 2019via nvd-ref
1 file changed · +6 2
  • packages/react-dev-utils/WebpackDevServerUtils.js+6 2 modified
    @@ -361,10 +361,14 @@ function prepareProxy(proxy, appPublicFolder) {
         process.exit(1);
       }
     
    -  // If proxy is specified, let it handle any request except for files in the public folder.
    +  // If proxy is specified, let it handle any request except for
    +  // files in the public folder and requests to the WebpackDevServer socket endpoint.
    +  // https://github.com/facebook/create-react-app/issues/6720
       function mayProxy(pathname) {
         const maybePublicPath = path.resolve(appPublicFolder, pathname.slice(1));
    -    return !fs.existsSync(maybePublicPath);
    +    const isPublicFileRequest = fs.existsSync(maybePublicPath);
    +    const isWdsEndpointRequest = pathname.startsWith('/sockjs-node'); // used by webpackHotDevClient
    +    return !(isPublicFileRequest || isWdsEndpointRequest);
       }
     
       if (!/^http(s)?:\/\//.test(proxy)) {
    

Vulnerability mechanics

Root cause

"The dev server's HMR WebSocket upgrade handler and the user-defined proxy's upgrade handler share the same 'upgrade' event, so a broadly-scoped proxy with ws: true intercepts HMR WebSocket connections intended for the built-in HMR server."

Attack vector

When webpack-dev-server is configured with a user-defined proxy that has a broad context (e.g. `/`) and `ws: true`, the dev server's own HMR WebSocket upgrade requests are forwarded to the proxy target instead of being handled by the built-in HMR server. An attacker on the same network or controlling the proxy target can intercept the browser's cookies and Origin header, bypass the dev server's Host/Origin validation, and corrupt the HMR socket — both the HMR and proxy subsystems write to the same socket [ref_id=1]. The fix in webpack-dev-server@5.2.5 [patch_id=6083971] adds a guard that short-circuits the proxy upgrade handler when the request URL matches the HMR WebSocket path (e.g. `/ws`).

What the fix does

The patch in `lib/Server.js` [patch_id=6083971] extracts the configured HMR WebSocket path (e.g. `/ws`) and, inside the `upgrade` event handler, compares the incoming request's URL pathname against that path. If they match, the proxy upgrade is skipped by returning early, leaving the built-in HMR server to handle the connection. The earlier workarounds in create-react-app [patch_id=6083969] and vue-cli [patch_id=6083970] took a different approach — they modified the `mayProxy` function to exclude `pathname.startsWith('/sockjs-node')` from the proxy context, preventing the SockJS-based HMR endpoint from being proxied.

Preconditions

  • configwebpack-dev-server must have a proxy configuration with a broad context (e.g. `/`) and `ws: true`.
  • networkThe attacker must be able to influence network traffic to the dev server (e.g. same network segment or controlling the proxy target) to observe the leaked HMR WebSocket or cookie/Origin data.

Generated on Jun 15, 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.