residoo 0.22.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/cve.js +278 -0
- package/src/integrity.js +155 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "residoo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"description": "Find secrets leaking through your AI coding agent's session history. Zero network calls in the scan path, zero dependencies.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "CloudRoam (https://cloudroam.io)",
|
package/src/cve.js
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A small, hand-curated, dated table of published CVEs affecting the
|
|
5
|
+
* MCP/AI-agent ecosystem, plus a minimal version-range matcher -- the
|
|
6
|
+
* shared infrastructure behind `residoo scan`'s MCP-config CVE check
|
|
7
|
+
* (integrity.js's "MCP server configuration risks" section) and a future
|
|
8
|
+
* `--project` dependency-manifest check.
|
|
9
|
+
*
|
|
10
|
+
* SOURCE, stated precisely because it matters: every entry below was
|
|
11
|
+
* fetched directly from GitHub's own Security Advisory REST API
|
|
12
|
+
* (api.github.com/advisories?ecosystem=<npm|pip>&affects=<package>) on
|
|
13
|
+
* 2026-09-05 -- a primary, authoritative source (GHSA-reviewed, each
|
|
14
|
+
* entry carrying its own CVE id, GHSA id, and vulnerable/patched version
|
|
15
|
+
* range), not a secondary aggregator's summary. An earlier draft of this
|
|
16
|
+
* table was built from a dedicated MCP-CVE tracking site
|
|
17
|
+
* (vulnerablemcp.info) and cross-checked against this API before
|
|
18
|
+
* shipping; every version range below is the API's own
|
|
19
|
+
* `vulnerable_version_range` field, not a re-derived or estimated one.
|
|
20
|
+
* Package NAMES were independently confirmed to exist on the real npm/
|
|
21
|
+
* PyPI registries (`npm view <pkg>`, PyPI's own JSON API) before being
|
|
22
|
+
* added here -- a wrong package name would silently never match anything,
|
|
23
|
+
* the same "never a false all-clear" concern CONTRIBUTING.md states for
|
|
24
|
+
* source paths, applied to this table instead.
|
|
25
|
+
*
|
|
26
|
+
* DELIBERATELY SMALL, on purpose: this is ~24 entries across 10 packages,
|
|
27
|
+
* not a claim of exhaustive coverage the way Medusa's own "~200 CVEs" is.
|
|
28
|
+
* Every single entry here is individually traceable to a specific GHSA/
|
|
29
|
+
* CVE id and a real advisory -- the same "84 high-confidence rules beat a
|
|
30
|
+
* competitor's higher rule count" trade-off already proven on this
|
|
31
|
+
* project's own benchmark (see bench/RESULTS.md), applied to CVE data
|
|
32
|
+
* instead of secret patterns. A welcome follow-up PR is adding more
|
|
33
|
+
* packages through the same API query, cited the same way -- never by
|
|
34
|
+
* guessing a plausible-sounding range.
|
|
35
|
+
*
|
|
36
|
+
* VERSION COMPARISON: a minimal, hand-written major.minor.patch numeric
|
|
37
|
+
* comparator -- NOT a full semver-range grammar (no ^, ~, prerelease
|
|
38
|
+
* tags, build metadata, x-ranges). Sufficient for every range in this
|
|
39
|
+
* table, including the date-based versioning scheme
|
|
40
|
+
* `@modelcontextprotocol/server-filesystem` switched to mid-2025
|
|
41
|
+
* (`2025.1.14`, `2025.7.1`) -- verified directly that comparing
|
|
42
|
+
* `[2025,1,20]` against `[2025,7,1]` component-by-component gives the
|
|
43
|
+
* correct ordering, the same way it would for an ordinary semver triple.
|
|
44
|
+
* A version string this comparator can't parse (a prerelease suffix, a
|
|
45
|
+
* git-hash pseudo-version, anything non-numeric) is reported as
|
|
46
|
+
* "cannot determine" by the caller, never silently treated as either safe
|
|
47
|
+
* or vulnerable -- CONTRIBUTING.md's rule 5 applied to a version string
|
|
48
|
+
* instead of a file.
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
const CVE_DATABASE = [
|
|
52
|
+
// ---- npm ----
|
|
53
|
+
{
|
|
54
|
+
id: "CVE-2025-6514", ecosystem: "npm", package: "mcp-remote", severity: "critical",
|
|
55
|
+
ranges: [{ min: "0.0.5", maxExclusive: "0.1.16" }],
|
|
56
|
+
summary: "OS command injection via a crafted authorization_endpoint response URL when connecting to an untrusted MCP server.",
|
|
57
|
+
source: "GHSA-6xpm-ggf7-wc3p",
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: "CVE-2025-58444", ecosystem: "npm", package: "@modelcontextprotocol/inspector", severity: "high",
|
|
61
|
+
ranges: [{ maxExclusive: "0.16.6" }],
|
|
62
|
+
summary: "Potential command execution via XSS when Inspector connects to an untrusted MCP server.",
|
|
63
|
+
source: "GHSA (fetched via api.github.com/advisories)",
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: "CVE-2025-49596", ecosystem: "npm", package: "@modelcontextprotocol/inspector", severity: "critical",
|
|
67
|
+
ranges: [{ maxExclusive: "0.14.1" }],
|
|
68
|
+
summary: "The Inspector proxy server lacks authentication between the Inspector client and the proxy, enabling RCE.",
|
|
69
|
+
source: "GHSA (fetched via api.github.com/advisories)",
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: "CVE-2026-23744", ecosystem: "npm", package: "@mcpjam/inspector", severity: "critical",
|
|
73
|
+
ranges: [{ maxInclusive: "1.4.2" }],
|
|
74
|
+
summary: "Remote code execution via an exposed, unauthenticated HTTP endpoint.",
|
|
75
|
+
source: "GHSA (fetched via api.github.com/advisories)",
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: "CVE-2025-65513", ecosystem: "npm", package: "mcp-fetch-server", severity: "medium",
|
|
79
|
+
ranges: [{ maxInclusive: "1.0.2" }],
|
|
80
|
+
summary: "Server-Side Request Forgery (SSRF) vulnerability.",
|
|
81
|
+
source: "GHSA (fetched via api.github.com/advisories)",
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: "CVE-2025-53372", ecosystem: "npm", package: "node-code-sandbox-mcp", severity: "high",
|
|
85
|
+
ranges: [{ maxInclusive: "1.2.0" }],
|
|
86
|
+
summary: "Sandbox escape via command injection.",
|
|
87
|
+
source: "GHSA (fetched via api.github.com/advisories)",
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: "CVE-2026-47250", ecosystem: "npm", package: "mcp-server-kubernetes", severity: "medium",
|
|
91
|
+
ranges: [{ maxInclusive: "3.6.2" }],
|
|
92
|
+
summary: "kubectl-generic flag injection enables Kubernetes bearer-token exfiltration.",
|
|
93
|
+
source: "GHSA (fetched via api.github.com/advisories)",
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
id: "CVE-2026-46519", ecosystem: "npm", package: "mcp-server-kubernetes", severity: "high",
|
|
97
|
+
ranges: [{ maxExclusive: "3.6.0" }],
|
|
98
|
+
summary: "Tool access control bypass via presentation-layer filtering with no execution-layer enforcement.",
|
|
99
|
+
source: "GHSA (fetched via api.github.com/advisories)",
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
id: "CVE-2026-39884", ecosystem: "npm", package: "mcp-server-kubernetes", severity: "high",
|
|
103
|
+
ranges: [{ maxInclusive: "3.4.0" }],
|
|
104
|
+
summary: "Argument injection in the port_forward tool via space-splitting.",
|
|
105
|
+
source: "GHSA (fetched via api.github.com/advisories)",
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
id: "CVE-2025-66404", ecosystem: "npm", package: "mcp-server-kubernetes", severity: "medium",
|
|
109
|
+
ranges: [{ maxInclusive: "2.9.7" }],
|
|
110
|
+
summary: "Security issue in the exec_in_pod tool.",
|
|
111
|
+
source: "GHSA (fetched via api.github.com/advisories)",
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
id: "CVE-2025-53355", ecosystem: "npm", package: "mcp-server-kubernetes", severity: "high",
|
|
115
|
+
ranges: [{ maxExclusive: "2.5.0" }],
|
|
116
|
+
summary: "Command injection in several tools.",
|
|
117
|
+
source: "GHSA (fetched via api.github.com/advisories)",
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
id: "CVE-2026-25536", ecosystem: "npm", package: "@modelcontextprotocol/sdk", severity: "high",
|
|
121
|
+
ranges: [{ min: "1.10.0", maxInclusive: "1.25.3" }],
|
|
122
|
+
summary: "Cross-client data leak via shared server/transport instance reuse.",
|
|
123
|
+
source: "GHSA (fetched via api.github.com/advisories)",
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: "CVE-2026-0621", ecosystem: "npm", package: "@modelcontextprotocol/sdk", severity: "high",
|
|
127
|
+
ranges: [{ min: "1.3.0", maxExclusive: "1.25.2" }],
|
|
128
|
+
summary: "ReDoS (regular expression denial of service) vulnerability.",
|
|
129
|
+
source: "GHSA (fetched via api.github.com/advisories)",
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
id: "CVE-2025-66414", ecosystem: "npm", package: "@modelcontextprotocol/sdk", severity: "high",
|
|
133
|
+
ranges: [{ maxExclusive: "1.24.0" }],
|
|
134
|
+
summary: "DNS rebinding protection not enabled by default on localhost-bound SSE/StreamableHTTP servers.",
|
|
135
|
+
source: "GHSA (fetched via api.github.com/advisories)",
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
id: "CVE-2025-53110", ecosystem: "npm", package: "@modelcontextprotocol/server-filesystem", severity: "high",
|
|
139
|
+
ranges: [{ maxInclusive: "0.6.2" }, { min: "2025.1.14", maxExclusive: "2025.7.1" }],
|
|
140
|
+
summary: "Path validation bypass via a colliding path prefix.",
|
|
141
|
+
source: "GHSA (fetched via api.github.com/advisories)",
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
id: "CVE-2025-53109", ecosystem: "npm", package: "@modelcontextprotocol/server-filesystem", severity: "high",
|
|
145
|
+
ranges: [{ maxInclusive: "0.6.2" }, { min: "2025.1.14", maxExclusive: "2025.7.1" }],
|
|
146
|
+
summary: "Path validation bypass via prefix matching and symlink handling.",
|
|
147
|
+
source: "GHSA (fetched via api.github.com/advisories)",
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
// ---- pip ----
|
|
151
|
+
{
|
|
152
|
+
id: "CVE-2026-59950", ecosystem: "pip", package: "mcp", severity: "high",
|
|
153
|
+
ranges: [{ maxExclusive: "1.28.1" }],
|
|
154
|
+
summary: "MCP Python SDK: WebSocket server transport does not support Host/Origin validation.",
|
|
155
|
+
source: "GHSA (fetched via api.github.com/advisories)",
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
id: "CVE-2026-52869", ecosystem: "pip", package: "mcp", severity: "high",
|
|
159
|
+
ranges: [{ maxInclusive: "1.27.1" }],
|
|
160
|
+
summary: "MCP Python SDK: HTTP transports serve session requests without verifying the authenticated principal.",
|
|
161
|
+
source: "GHSA (fetched via api.github.com/advisories)",
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
id: "CVE-2026-52870", ecosystem: "pip", package: "mcp", severity: "high",
|
|
165
|
+
ranges: [{ min: "1.23.0", maxInclusive: "1.27.1" }],
|
|
166
|
+
summary: "Experimental task handlers allow any client to access and cancel other clients' tasks.",
|
|
167
|
+
source: "GHSA (fetched via api.github.com/advisories)",
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
id: "CVE-2025-66416", ecosystem: "pip", package: "mcp", severity: "high",
|
|
171
|
+
ranges: [{ maxExclusive: "1.23.0" }],
|
|
172
|
+
summary: "DNS rebinding protection not enabled by default on localhost-bound SSE/StreamableHTTP servers.",
|
|
173
|
+
source: "GHSA (fetched via api.github.com/advisories)",
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
id: "CVE-2025-53366", ecosystem: "pip", package: "mcp", severity: "high",
|
|
177
|
+
ranges: [{ maxExclusive: "1.9.4" }],
|
|
178
|
+
summary: "FastMCP Server validation error leading to denial of service.",
|
|
179
|
+
source: "GHSA (fetched via api.github.com/advisories)",
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
id: "CVE-2025-53365", ecosystem: "pip", package: "mcp", severity: "high",
|
|
183
|
+
ranges: [{ maxExclusive: "1.10.0" }],
|
|
184
|
+
summary: "Unhandled exception in the Streamable HTTP transport, leading to denial of service.",
|
|
185
|
+
source: "GHSA (fetched via api.github.com/advisories)",
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
id: "CVE-2026-27735", ecosystem: "pip", package: "mcp-server-git", severity: "medium",
|
|
189
|
+
ranges: [{ maxExclusive: "2026.1.14" }],
|
|
190
|
+
summary: "Path traversal in git_add allows staging files outside the repository boundary.",
|
|
191
|
+
source: "GHSA (fetched via api.github.com/advisories)",
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
id: "CVE-2025-68145", ecosystem: "pip", package: "mcp-server-git", severity: "medium",
|
|
195
|
+
ranges: [{ maxExclusive: "2025.12.18" }],
|
|
196
|
+
summary: "Missing path validation when using the --repository flag.",
|
|
197
|
+
source: "GHSA (fetched via api.github.com/advisories)",
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
id: "CVE-2025-68144", ecosystem: "pip", package: "mcp-server-git", severity: "medium",
|
|
201
|
+
ranges: [{ maxExclusive: "2025.12.18" }],
|
|
202
|
+
summary: "Argument injection in git_diff and git_checkout allows overwriting local files.",
|
|
203
|
+
source: "GHSA (fetched via api.github.com/advisories)",
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
id: "CVE-2025-68143", ecosystem: "pip", package: "mcp-server-git", severity: "medium",
|
|
207
|
+
ranges: [{ maxExclusive: "2025.9.25" }],
|
|
208
|
+
summary: "Unrestricted git_init tool allows repository creation at arbitrary filesystem locations.",
|
|
209
|
+
source: "GHSA (fetched via api.github.com/advisories)",
|
|
210
|
+
},
|
|
211
|
+
];
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Parse a version string's leading major.minor.patch numeric triple.
|
|
215
|
+
* Returns null for anything this simple comparator can't handle (a
|
|
216
|
+
* prerelease/build-metadata suffix, a git-hash pseudo-version, a bare "1"
|
|
217
|
+
* or "1.2" with no patch component) -- the caller must treat null as
|
|
218
|
+
* "cannot determine," never as a match or a clean bill of health.
|
|
219
|
+
*/
|
|
220
|
+
function parseVersion(v) {
|
|
221
|
+
// `String(v)` itself can throw -- an object whose own `toString` property
|
|
222
|
+
// is present but not a function (e.g. `{ toString: "" }`, a real shape
|
|
223
|
+
// fast-check's property fuzzing found, not a hypothetical) fails the
|
|
224
|
+
// ToPrimitive coercion with "Cannot convert object to primitive value"
|
|
225
|
+
// before the regex ever runs. `v` here can be anything an attacker-
|
|
226
|
+
// controlled MCP config's `args` array contains, so this must degrade to
|
|
227
|
+
// "unparseable" the same as any other malformed input, never throw.
|
|
228
|
+
let s;
|
|
229
|
+
try { s = String(v).trim(); } catch { return null; }
|
|
230
|
+
const m = /^v?(\d+)\.(\d+)\.(\d+)/.exec(s);
|
|
231
|
+
if (!m) return null;
|
|
232
|
+
return [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** -1 / 0 / 1, comparing two parseVersion() triples component-by-component. */
|
|
236
|
+
function compareVersions(a, b) {
|
|
237
|
+
for (let i = 0; i < 3; i++) {
|
|
238
|
+
if (a[i] !== b[i]) return a[i] - b[i];
|
|
239
|
+
}
|
|
240
|
+
return 0;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function inOneRange(parsed, range) {
|
|
244
|
+
if (range.min) {
|
|
245
|
+
const min = parseVersion(range.min);
|
|
246
|
+
if (min && compareVersions(parsed, min) < 0) return false;
|
|
247
|
+
}
|
|
248
|
+
if (range.maxInclusive) {
|
|
249
|
+
const max = parseVersion(range.maxInclusive);
|
|
250
|
+
if (max && compareVersions(parsed, max) > 0) return false;
|
|
251
|
+
}
|
|
252
|
+
if (range.maxExclusive) {
|
|
253
|
+
const max = parseVersion(range.maxExclusive);
|
|
254
|
+
if (max && compareVersions(parsed, max) >= 0) return false;
|
|
255
|
+
}
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Check one resolved `packageName`@`version` pair (ecosystem "npm" or
|
|
261
|
+
* "pip") against CVE_DATABASE. Returns `{ checkable, matches }` --
|
|
262
|
+
* `checkable: false` means the version string couldn't be parsed at all
|
|
263
|
+
* (report "cannot determine," never silently clean); `matches` is every
|
|
264
|
+
* CVE_DATABASE entry whose package matches and whose vulnerable range
|
|
265
|
+
* (any one of its, possibly several, disjoint ranges) contains this
|
|
266
|
+
* version.
|
|
267
|
+
*/
|
|
268
|
+
function checkVersion(ecosystem, packageName, version) {
|
|
269
|
+
const parsed = parseVersion(version);
|
|
270
|
+
if (!parsed) return { checkable: false, matches: [] };
|
|
271
|
+
const matches = CVE_DATABASE.filter((entry) =>
|
|
272
|
+
entry.ecosystem === ecosystem &&
|
|
273
|
+
entry.package === packageName &&
|
|
274
|
+
entry.ranges.some((r) => inOneRange(parsed, r)));
|
|
275
|
+
return { checkable: true, matches };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
module.exports = { CVE_DATABASE, parseVersion, compareVersions, checkVersion };
|
package/src/integrity.js
CHANGED
|
@@ -10,6 +10,7 @@ const os = require("os");
|
|
|
10
10
|
// reference at import time and never see a later patch.
|
|
11
11
|
const cp = require("child_process");
|
|
12
12
|
const { PATTERNS, redact } = require("./patterns");
|
|
13
|
+
const { checkVersion } = require("./cve");
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Integrity checks for agent config directories.
|
|
@@ -386,6 +387,100 @@ function extractHooks(parsed) {
|
|
|
386
387
|
return { hooks, unrecognized, hadHooksKey: true, truncated, sawLeaf };
|
|
387
388
|
}
|
|
388
389
|
|
|
390
|
+
// ── MCP server configuration risk checks ────────────────────────────────
|
|
391
|
+
//
|
|
392
|
+
// Distinct from agent-configs.js (which scans these same files' raw TEXT
|
|
393
|
+
// for leaked secrets) and from injection.js (which scans transcript
|
|
394
|
+
// CONTENT for injection payloads): this parses the STRUCTURED
|
|
395
|
+
// `mcpServers` object every MCP client config converges on
|
|
396
|
+
// (`{"mcpServers": {"<name>": {"command", "args", "env", "url"}}}` --
|
|
397
|
+
// Claude Code, Claude Desktop, Cursor, and Visual Studio's own docs all
|
|
398
|
+
// document this identical shape, already cited in agent-configs.js's own
|
|
399
|
+
// header) and checks it for two named, real risk classes:
|
|
400
|
+
//
|
|
401
|
+
// 1. A known-vulnerable version of a specific MCP-ecosystem package
|
|
402
|
+
// pinned in the server's launch command (see cve.js).
|
|
403
|
+
// 2. A remote (non-loopback) server configured over plain HTTP instead
|
|
404
|
+
// of HTTPS -- MCP protocol traffic, including tool definitions,
|
|
405
|
+
// crossing a real network in cleartext.
|
|
406
|
+
//
|
|
407
|
+
// NOT checked, disclosed rather than silently gapped (see injection.js's
|
|
408
|
+
// own header for the fuller version of this point): a malicious server
|
|
409
|
+
// changing a TOOL'S OWN DESCRIPTION after approval ("tool poisoning") --
|
|
410
|
+
// a tool's description is protocol data returned by a live server at
|
|
411
|
+
// request time, never present in a static config file, so there is
|
|
412
|
+
// nothing here for a file-scanner to check it against.
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Extract the `mcpServers` object from a parsed config file. Every real
|
|
416
|
+
* MCP client this project has verified (Claude Code, Claude Desktop,
|
|
417
|
+
* Cursor, Visual Studio) uses this identical top-level shape. Anything
|
|
418
|
+
* else (the key absent, present but not an object) yields an empty
|
|
419
|
+
* object -- a config with no MCP servers configured is not an error.
|
|
420
|
+
*/
|
|
421
|
+
function extractMcpServers(parsed) {
|
|
422
|
+
if (!parsed || typeof parsed !== "object") return {};
|
|
423
|
+
const servers = parsed.mcpServers;
|
|
424
|
+
if (!servers || typeof servers !== "object" || Array.isArray(servers)) return {};
|
|
425
|
+
return servers;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// npm: optional @scope/, then a name, a literal @, then a version
|
|
429
|
+
// starting with a digit. pip: a name, a literal ==, then a version
|
|
430
|
+
// starting with a digit. Both anchored full-string (^...$) -- matched
|
|
431
|
+
// against ONE argv element at a time, never a substring of a longer
|
|
432
|
+
// string that happens to contain a coincidental match.
|
|
433
|
+
const NPM_PKG_VERSION_RE = /^(@[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+|[a-zA-Z0-9._-]+)@(\d[\w.-]*)$/;
|
|
434
|
+
const PIP_PKG_VERSION_RE = /^([a-zA-Z0-9._-]+)==(\d[\w.-]*)$/;
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Scan one MCP server's `args` array for a `package@version` (npm) or
|
|
438
|
+
* `package==version` (pip) argv element -- the documented invocation
|
|
439
|
+
* shape for `npx <pkg>@<version>` / `uvx <pkg>==<version>` / `pip install
|
|
440
|
+
* <pkg>==<version>`. An unpinned invocation (`npx mcp-remote` with no
|
|
441
|
+
* `@version`) yields nothing here on purpose: there's no version to
|
|
442
|
+
* check, and this deliberately does NOT treat "unpinned" itself as a
|
|
443
|
+
* finding -- that's a real, genuinely debatable trade-off (always
|
|
444
|
+
* getting the latest PATCHED release vs. exposure to a compromised
|
|
445
|
+
* "latest" publish), not a clear vulnerability the way a specific
|
|
446
|
+
* known-bad pinned version is.
|
|
447
|
+
*/
|
|
448
|
+
function extractPackageVersions(args) {
|
|
449
|
+
if (!Array.isArray(args)) return [];
|
|
450
|
+
const found = [];
|
|
451
|
+
for (const a of args) {
|
|
452
|
+
if (typeof a !== "string") continue;
|
|
453
|
+
const npm = NPM_PKG_VERSION_RE.exec(a);
|
|
454
|
+
if (npm) { found.push({ ecosystem: "npm", package: npm[1], version: npm[2] }); continue; }
|
|
455
|
+
const pip = PIP_PKG_VERSION_RE.exec(a);
|
|
456
|
+
if (pip) found.push({ ecosystem: "pip", package: pip[1], version: pip[2] });
|
|
457
|
+
}
|
|
458
|
+
return found;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// A well-known, widely-flagged supply-chain red flag: an installer script
|
|
462
|
+
// fetched and piped directly into a shell, never saved or reviewed first.
|
|
463
|
+
// Checked against the whole command+args joined, not `command` alone,
|
|
464
|
+
// since the realistic shape is `command: "sh", args: ["-c", "curl ... |
|
|
465
|
+
// bash"]` -- the risky text lives in `args`, not `command`.
|
|
466
|
+
const CURL_PIPE_SHELL_RE = /\b(?:curl|wget)\b[^|]*\|\s*(?:sudo\s+)?(?:sh|bash|zsh)\b/i;
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* True for a URL-transport MCP server (`url` field present) using plain
|
|
470
|
+
* HTTP to a non-loopback host. A loopback URL (localhost/127.0.0.1/::1)
|
|
471
|
+
* is excluded -- an ordinary local-dev MCP server, not the actual risk
|
|
472
|
+
* this names: MCP protocol traffic, including tool definitions, crossing
|
|
473
|
+
* a real network where it can be observed or altered in transit.
|
|
474
|
+
*/
|
|
475
|
+
function isInsecureRemoteUrl(url) {
|
|
476
|
+
if (typeof url !== "string") return false;
|
|
477
|
+
let parsed;
|
|
478
|
+
try { parsed = new URL(url); } catch { return false; }
|
|
479
|
+
if (parsed.protocol !== "http:") return false;
|
|
480
|
+
const host = parsed.hostname;
|
|
481
|
+
return host !== "localhost" && host !== "127.0.0.1" && host !== "::1" && host !== "[::1]";
|
|
482
|
+
}
|
|
483
|
+
|
|
389
484
|
// ── the checker ───────────────────────────────────────────────────────────
|
|
390
485
|
|
|
391
486
|
/**
|
|
@@ -827,6 +922,66 @@ function checkIntegrity({ home = os.homedir(), cwd = process.cwd(), projectMode
|
|
|
827
922
|
}
|
|
828
923
|
}
|
|
829
924
|
|
|
925
|
+
// ---- 6. MCP server configuration risks ---------------------------------
|
|
926
|
+
// Home/machine-level configs (none of these are ever project-scoped by
|
|
927
|
+
// any vendor's own design, the same reasoning section 5 states) plus
|
|
928
|
+
// the two genuinely project-scoped MCP config locations, which use `cwd`
|
|
929
|
+
// unconditionally -- `cwd` already equals the project root in project
|
|
930
|
+
// mode (see this function's own docstring), so no separate branch is
|
|
931
|
+
// needed for them.
|
|
932
|
+
const mcpConfigFiles = [];
|
|
933
|
+
if (!projectMode) {
|
|
934
|
+
mcpConfigFiles.push(path.join(home, ".claude.json"));
|
|
935
|
+
const desktopConfig =
|
|
936
|
+
process.platform === "darwin" ? path.join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json") :
|
|
937
|
+
process.platform === "win32" ? path.join(process.env.APPDATA || path.join(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json") :
|
|
938
|
+
null; // Linux: no official build, unofficial ports disagree -- see agent-configs.js's own header
|
|
939
|
+
if (desktopConfig) mcpConfigFiles.push(desktopConfig);
|
|
940
|
+
mcpConfigFiles.push(path.join(home, ".cursor", "mcp.json"));
|
|
941
|
+
mcpConfigFiles.push(path.join(home, ".kiro", "settings", "mcp.json"));
|
|
942
|
+
mcpConfigFiles.push(path.join(home, ".mcp.json"));
|
|
943
|
+
}
|
|
944
|
+
mcpConfigFiles.push(path.join(cwd, ".mcp.json"));
|
|
945
|
+
mcpConfigFiles.push(path.join(cwd, ".vs", "mcp.json"));
|
|
946
|
+
|
|
947
|
+
for (const file of mcpConfigFiles) {
|
|
948
|
+
const text = readOrReport(file);
|
|
949
|
+
if (text === null) continue;
|
|
950
|
+
|
|
951
|
+
let parsed;
|
|
952
|
+
try { parsed = JSON.parse(text); } catch {
|
|
953
|
+
add("warn", "unparseable-config", file, "exists in a known MCP config location but is not valid JSON; the client's own loader would also choke on it, so corruption or tampering is worth a look");
|
|
954
|
+
continue;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
const servers = extractMcpServers(parsed);
|
|
958
|
+
for (const [name, def] of Object.entries(servers)) {
|
|
959
|
+
if (!def || typeof def !== "object") continue;
|
|
960
|
+
const label = safePreview(name, 60);
|
|
961
|
+
|
|
962
|
+
for (const { ecosystem, package: pkg, version } of extractPackageVersions(def.args)) {
|
|
963
|
+
const { checkable, matches } = checkVersion(ecosystem, pkg, version);
|
|
964
|
+
if (!checkable) continue; // a version string this comparator can't parse: unverified, not silently clean, but nothing more to say than that
|
|
965
|
+
for (const m of matches) {
|
|
966
|
+
add("warn", "mcp-known-cve", file,
|
|
967
|
+
`MCP server "${label}" pins ${pkg}@${version}, matching ${m.id} (${m.severity}): ${m.summary} Upgrade past the vulnerable range.`);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
if (isInsecureRemoteUrl(def.url)) {
|
|
972
|
+
add("warn", "mcp-insecure-transport", file,
|
|
973
|
+
`MCP server "${label}" connects to ${safePreview(def.url, 80)} over plain HTTP -- protocol traffic, including tool definitions, can be observed or altered in transit. Use an HTTPS URL if the server supports one.`);
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
const commandLine = [def.command, ...(Array.isArray(def.args) ? def.args : [])]
|
|
977
|
+
.filter((x) => typeof x === "string").join(" ");
|
|
978
|
+
if (commandLine && CURL_PIPE_SHELL_RE.test(commandLine)) {
|
|
979
|
+
add("warn", "mcp-curl-pipe-shell", file,
|
|
980
|
+
`MCP server "${label}" launch command fetches a script and pipes it directly into a shell: "${safePreview(commandLine)}". Review and pin to a specific, reviewed version instead of trusting whatever the URL currently serves.`);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
830
985
|
return {
|
|
831
986
|
findings,
|
|
832
987
|
filesChecked,
|