mcptrustchecker 1.0.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/LICENSE +21 -0
- package/README.md +379 -0
- package/dist/acquire/clientConfig.d.ts +47 -0
- package/dist/acquire/clientConfig.js +124 -0
- package/dist/acquire/discover.d.ts +12 -0
- package/dist/acquire/discover.js +53 -0
- package/dist/acquire/index.d.ts +24 -0
- package/dist/acquire/index.js +175 -0
- package/dist/acquire/live.d.ts +47 -0
- package/dist/acquire/live.js +357 -0
- package/dist/acquire/manifest.d.ts +10 -0
- package/dist/acquire/manifest.js +94 -0
- package/dist/acquire/npm.d.ts +10 -0
- package/dist/acquire/npm.js +55 -0
- package/dist/acquire/source.d.ts +16 -0
- package/dist/acquire/source.js +109 -0
- package/dist/cli/index.d.ts +15 -0
- package/dist/cli/index.js +396 -0
- package/dist/config.d.ts +15 -0
- package/dist/config.js +81 -0
- package/dist/data/capabilityLexicon.d.ts +51 -0
- package/dist/data/capabilityLexicon.js +204 -0
- package/dist/data/confusables.d.ts +17 -0
- package/dist/data/confusables.js +65 -0
- package/dist/data/injectionPatterns.d.ts +55 -0
- package/dist/data/injectionPatterns.js +216 -0
- package/dist/data/knownCves.d.ts +24 -0
- package/dist/data/knownCves.js +49 -0
- package/dist/data/protectedPackages.d.ts +30 -0
- package/dist/data/protectedPackages.js +89 -0
- package/dist/data/ruleCatalog.d.ts +16 -0
- package/dist/data/ruleCatalog.js +94 -0
- package/dist/data/sourcePatterns.d.ts +25 -0
- package/dist/data/sourcePatterns.js +92 -0
- package/dist/data/unicode.d.ts +34 -0
- package/dist/data/unicode.js +99 -0
- package/dist/detectors/capability.d.ts +14 -0
- package/dist/detectors/capability.js +281 -0
- package/dist/detectors/collision.d.ts +11 -0
- package/dist/detectors/collision.js +63 -0
- package/dist/detectors/flowGraph.d.ts +43 -0
- package/dist/detectors/flowGraph.js +100 -0
- package/dist/detectors/index.d.ts +18 -0
- package/dist/detectors/index.js +26 -0
- package/dist/detectors/injection.d.ts +9 -0
- package/dist/detectors/injection.js +178 -0
- package/dist/detectors/meta.d.ts +8 -0
- package/dist/detectors/meta.js +33 -0
- package/dist/detectors/posture.d.ts +9 -0
- package/dist/detectors/posture.js +128 -0
- package/dist/detectors/source.d.ts +12 -0
- package/dist/detectors/source.js +87 -0
- package/dist/detectors/supplyChain.d.ts +17 -0
- package/dist/detectors/supplyChain.js +243 -0
- package/dist/detectors/toxicFlow.d.ts +22 -0
- package/dist/detectors/toxicFlow.js +201 -0
- package/dist/detectors/unicode.d.ts +21 -0
- package/dist/detectors/unicode.js +244 -0
- package/dist/engine.d.ts +27 -0
- package/dist/engine.js +135 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.js +46 -0
- package/dist/lockfile.d.ts +30 -0
- package/dist/lockfile.js +87 -0
- package/dist/policy.d.ts +13 -0
- package/dist/policy.js +48 -0
- package/dist/report/badge.d.ts +14 -0
- package/dist/report/badge.js +18 -0
- package/dist/report/json.d.ts +4 -0
- package/dist/report/json.js +4 -0
- package/dist/report/markdown.d.ts +6 -0
- package/dist/report/markdown.js +110 -0
- package/dist/report/sarif.d.ts +7 -0
- package/dist/report/sarif.js +105 -0
- package/dist/report/terminal.d.ts +15 -0
- package/dist/report/terminal.js +252 -0
- package/dist/scoring/capability.d.ts +10 -0
- package/dist/scoring/capability.js +48 -0
- package/dist/scoring/index.d.ts +10 -0
- package/dist/scoring/index.js +96 -0
- package/dist/scoring/model.d.ts +50 -0
- package/dist/scoring/model.js +101 -0
- package/dist/types.d.ts +361 -0
- package/dist/types.js +12 -0
- package/dist/util/ansi.d.ts +26 -0
- package/dist/util/ansi.js +42 -0
- package/dist/util/capabilities.d.ts +17 -0
- package/dist/util/capabilities.js +68 -0
- package/dist/util/distance.d.ts +23 -0
- package/dist/util/distance.js +119 -0
- package/dist/util/hash.d.ts +20 -0
- package/dist/util/hash.js +80 -0
- package/dist/util/text.d.ts +45 -0
- package/dist/util/text.js +170 -0
- package/dist/version.d.ts +13 -0
- package/dist/version.js +13 -0
- package/package.json +72 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/*! MCP Trust Checker · https://mcptrustchecker.com · support@mcptrustchecker.com · © 2026 Illia Haidar · MIT */
|
|
2
|
+
/**
|
|
3
|
+
* Stage 2 — Content injection heuristics (tool poisoning / line jumping /
|
|
4
|
+
* shadowing). Runs the pattern lexicon across four channels, then escalates
|
|
5
|
+
* when multiple poisoning signals co-occur in one field — the shape of a real
|
|
6
|
+
* tool-poisoning attack rather than an incidental keyword.
|
|
7
|
+
*/
|
|
8
|
+
import { collectTextFields } from '../util/text.js';
|
|
9
|
+
import { compiledInjectionPatterns, longestAllCapsRun, hasBase64Blob } from '../util/text.js';
|
|
10
|
+
import { ALLCAPS_RUN_WORDS, SECRET_PATTERNS, SUSPICIOUS_PARAM_NAMES, SUSPICIOUS_URL_HOST, SUSPICIOUS_URL_PATH, URL_IP_LITERAL, } from '../data/injectionPatterns.js';
|
|
11
|
+
const PATTERNS = compiledInjectionPatterns();
|
|
12
|
+
function firstMatch(regex, text) {
|
|
13
|
+
const m = text.match(regex);
|
|
14
|
+
return m ? m[0] : undefined;
|
|
15
|
+
}
|
|
16
|
+
export const injectionDetector = {
|
|
17
|
+
id: 'injection',
|
|
18
|
+
stage: 2,
|
|
19
|
+
title: 'Prompt-injection / tool-poisoning heuristics',
|
|
20
|
+
run(ctx) {
|
|
21
|
+
const findings = [];
|
|
22
|
+
for (const field of collectTextFields(ctx.surface)) {
|
|
23
|
+
const kindsHit = new Set();
|
|
24
|
+
for (const p of PATTERNS) {
|
|
25
|
+
if (!p.meta.channels.includes(field.channel))
|
|
26
|
+
continue;
|
|
27
|
+
const match = firstMatch(p.regex, field.text);
|
|
28
|
+
if (!match)
|
|
29
|
+
continue;
|
|
30
|
+
kindsHit.add(p.kind);
|
|
31
|
+
findings.push({
|
|
32
|
+
ruleId: p.id,
|
|
33
|
+
title: p.meta.title,
|
|
34
|
+
category: 'injection',
|
|
35
|
+
severity: p.meta.baseSeverity,
|
|
36
|
+
confidence: p.meta.baseConfidence,
|
|
37
|
+
description: `${p.meta.title} detected in the ${field.location.field ?? 'text'} of ` +
|
|
38
|
+
`${field.location.kind}${field.location.name ? ` "${field.location.name}"` : ''}. ` +
|
|
39
|
+
`Instruction-like content in tool metadata is executed by the model, not the human, and is the ` +
|
|
40
|
+
`primary tool-poisoning vector.`,
|
|
41
|
+
remediation: 'Tool descriptions should describe behavior, not instruct the assistant. Treat imperative / ' +
|
|
42
|
+
'secrecy / sequencing language in metadata as hostile.',
|
|
43
|
+
location: field.location,
|
|
44
|
+
evidence: match.slice(0, 160),
|
|
45
|
+
owasp: 'LLM01:2025 Prompt Injection',
|
|
46
|
+
data: { kind: p.kind },
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
// Escalation to CRITICAL requires a genuinely malicious signal — secrecy
|
|
50
|
+
// (conceal from the user) or an instruction override — co-occurring with
|
|
51
|
+
// at least one other poisoning signal. A mere sensitive-target reference
|
|
52
|
+
// is NOT enough (legit tools reference credentials/context), nor is plain
|
|
53
|
+
// emphasis + sequencing ("IMPORTANT: call X first").
|
|
54
|
+
const strongSignal = kindsHit.has('secrecy') || kindsHit.has('override');
|
|
55
|
+
if (strongSignal && kindsHit.size >= 2) {
|
|
56
|
+
findings.push({
|
|
57
|
+
ruleId: 'MTC-INJ-POISON',
|
|
58
|
+
title: 'Compound tool-poisoning pattern',
|
|
59
|
+
category: 'injection',
|
|
60
|
+
severity: 'critical',
|
|
61
|
+
confidence: 'strong',
|
|
62
|
+
description: `Multiple tool-poisoning signals co-occur in a single field (${[...kindsHit].join(', ')}), including a ` +
|
|
63
|
+
`concealment / override / sensitive-target directive. Together they form an instruction aimed at the ` +
|
|
64
|
+
`model — the canonical tool-poisoning shape, not ordinary documentation.`,
|
|
65
|
+
remediation: 'Do not install this server; the metadata is engineered to manipulate the assistant.',
|
|
66
|
+
location: field.location,
|
|
67
|
+
evidence: field.text.slice(0, 200),
|
|
68
|
+
owasp: 'LLM01:2025 Prompt Injection',
|
|
69
|
+
data: { kinds: [...kindsHit] },
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
// ALL-CAPS shouting (weak signal, contextual).
|
|
73
|
+
if ((field.channel === 'tool-description' ||
|
|
74
|
+
field.channel === 'param-description' ||
|
|
75
|
+
field.channel === 'server-instructions') &&
|
|
76
|
+
longestAllCapsRun(field.text) >= ALLCAPS_RUN_WORDS) {
|
|
77
|
+
findings.push({
|
|
78
|
+
ruleId: 'MTC-INJ-CAPS',
|
|
79
|
+
title: 'Excessive ALL-CAPS emphasis in metadata',
|
|
80
|
+
category: 'injection',
|
|
81
|
+
severity: 'low',
|
|
82
|
+
confidence: 'heuristic',
|
|
83
|
+
description: 'A run of shouted words is a common way to give injected instructions false authority.',
|
|
84
|
+
location: field.location,
|
|
85
|
+
evidence: field.text.slice(0, 120),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
// Base64 blob in prose paired with an explicit decode/execute verb.
|
|
89
|
+
// Requires an actual DECODE verb near a blob — "execute"/"eval" describing
|
|
90
|
+
// a tool's normal function (e.g. "execute batched operations") must not match.
|
|
91
|
+
if (field.channel !== 'other' &&
|
|
92
|
+
hasBase64Blob(field.text) &&
|
|
93
|
+
/\b(decode|base64|atob|b64decode|un-?base64)\b/i.test(field.text)) {
|
|
94
|
+
findings.push({
|
|
95
|
+
ruleId: 'MTC-INJ-ENC-2',
|
|
96
|
+
title: 'Encoded blob paired with a decode/execute instruction',
|
|
97
|
+
category: 'injection',
|
|
98
|
+
severity: 'high',
|
|
99
|
+
confidence: 'strong',
|
|
100
|
+
description: 'A long base64-looking blob appears alongside a decode/execute instruction — a common way to hide a payload from reviewers.',
|
|
101
|
+
location: field.location,
|
|
102
|
+
evidence: field.text.slice(0, 160),
|
|
103
|
+
owasp: 'LLM01:2025 Prompt Injection',
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
// Embedded secret VALUE (a real credential, not merely a reference).
|
|
107
|
+
for (const sp of SECRET_PATTERNS) {
|
|
108
|
+
const m = field.text.match(sp.pattern);
|
|
109
|
+
if (m) {
|
|
110
|
+
findings.push({
|
|
111
|
+
ruleId: 'MTC-INJ-SECRET-1',
|
|
112
|
+
title: `Embedded ${sp.label} in ${field.location.field ?? 'metadata'}`,
|
|
113
|
+
category: 'exfiltration',
|
|
114
|
+
severity: 'high',
|
|
115
|
+
confidence: 'confirmed',
|
|
116
|
+
description: `A live-looking ${sp.label} is embedded in the ${field.location.field ?? 'metadata'} of ` +
|
|
117
|
+
`${field.location.kind}${field.location.name ? ` "${field.location.name}"` : ''}. Hardcoded ` +
|
|
118
|
+
`credentials in server metadata leak to every client that lists this server.`,
|
|
119
|
+
remediation: 'Remove the credential and rotate it; never ship secrets in tool metadata.',
|
|
120
|
+
location: field.location,
|
|
121
|
+
evidence: `${sp.label}: ${m[0].slice(0, 4)}…(redacted)`,
|
|
122
|
+
owasp: 'LLM02:2025 Sensitive Information Disclosure',
|
|
123
|
+
data: { secretType: sp.id },
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Suspicious external URL in metadata — a hardcoded webhook/paste/exfil
|
|
128
|
+
// endpoint or raw IP in a tool description is a data-exfiltration channel
|
|
129
|
+
// (the malicious-URL tool-poisoning move: "send the result to …").
|
|
130
|
+
for (const um of field.text.matchAll(/\bhttps?:\/\/([^\s/"'`)\]]+)(\/[^\s"'`)]*)?/gi)) {
|
|
131
|
+
const host = (um[1] ?? '').toLowerCase();
|
|
132
|
+
const rest = um[2] ?? '';
|
|
133
|
+
if (SUSPICIOUS_URL_HOST.test(host) || URL_IP_LITERAL.test(host) || SUSPICIOUS_URL_PATH.test(host + rest)) {
|
|
134
|
+
findings.push({
|
|
135
|
+
ruleId: 'MTC-INJ-URL-1',
|
|
136
|
+
title: 'Suspicious external URL in tool metadata',
|
|
137
|
+
category: 'exfiltration',
|
|
138
|
+
severity: 'medium',
|
|
139
|
+
confidence: 'strong',
|
|
140
|
+
description: `A hardcoded link to a request/paste/webhook sink (or raw IP) appears in the ` +
|
|
141
|
+
`${field.location.field ?? 'metadata'} of ${field.location.kind}` +
|
|
142
|
+
`${field.location.name ? ` "${field.location.name}"` : ''}. Tool metadata pointing the model at a ` +
|
|
143
|
+
`fixed external endpoint is a data-exfiltration channel — the classic "send the output to …" poisoning.`,
|
|
144
|
+
remediation: 'A legitimate tool takes its destination as a validated parameter; it does not hardcode a webhook/paste sink in its description.',
|
|
145
|
+
location: field.location,
|
|
146
|
+
evidence: um[0].slice(0, 120),
|
|
147
|
+
owasp: 'LLM02:2025 Sensitive Information Disclosure',
|
|
148
|
+
});
|
|
149
|
+
break; // one per field is enough
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// Suspicious hidden-parameter names.
|
|
154
|
+
for (const tool of ctx.surface.tools) {
|
|
155
|
+
if (!tool || typeof tool !== 'object')
|
|
156
|
+
continue;
|
|
157
|
+
const props = tool.inputSchema?.properties ?? {};
|
|
158
|
+
for (const paramName of Object.keys(props)) {
|
|
159
|
+
if (SUSPICIOUS_PARAM_NAMES.includes(paramName.toLowerCase())) {
|
|
160
|
+
findings.push({
|
|
161
|
+
ruleId: 'MTC-INJ-PARAM',
|
|
162
|
+
title: `Suspicious hidden-channel parameter "${paramName}"`,
|
|
163
|
+
category: 'injection',
|
|
164
|
+
severity: 'medium',
|
|
165
|
+
confidence: 'heuristic',
|
|
166
|
+
description: `Tool "${tool.name}" exposes a parameter named "${paramName}", a name frequently used as a ` +
|
|
167
|
+
`hidden exfiltration channel (the model is told to stuff context/history/secrets into it).`,
|
|
168
|
+
remediation: 'Verify what this parameter is actually used for; hidden "context"/"note" params are a red flag.',
|
|
169
|
+
location: { kind: 'tool', name: tool.name, field: `inputSchema.properties.${paramName}` },
|
|
170
|
+
owasp: 'LLM01:2025 Prompt Injection',
|
|
171
|
+
data: { param: paramName },
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return findings;
|
|
177
|
+
},
|
|
178
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/*! MCP Trust Checker · https://mcptrustchecker.com · support@mcptrustchecker.com · © 2026 Illia Haidar · MIT */
|
|
2
|
+
/**
|
|
3
|
+
* Meta / hygiene checks about the scan itself — most importantly, flagging when
|
|
4
|
+
* a surface is empty so a malformed or wrong-shape manifest is never mistaken
|
|
5
|
+
* for a clean, safe server.
|
|
6
|
+
*/
|
|
7
|
+
import type { Detector } from '../types.js';
|
|
8
|
+
export declare const metaDetector: Detector;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/*! MCP Trust Checker · https://mcptrustchecker.com · support@mcptrustchecker.com · © 2026 Illia Haidar · MIT */
|
|
2
|
+
/**
|
|
3
|
+
* Meta / hygiene checks about the scan itself — most importantly, flagging when
|
|
4
|
+
* a surface is empty so a malformed or wrong-shape manifest is never mistaken
|
|
5
|
+
* for a clean, safe server.
|
|
6
|
+
*/
|
|
7
|
+
export const metaDetector = {
|
|
8
|
+
id: 'meta',
|
|
9
|
+
stage: 0,
|
|
10
|
+
title: 'Scan metadata',
|
|
11
|
+
run(ctx) {
|
|
12
|
+
const { surface } = ctx;
|
|
13
|
+
const empty = surface.tools.length === 0 && surface.prompts.length === 0 && surface.resources.length === 0;
|
|
14
|
+
// A package-only scan legitimately has no live surface; don't warn there.
|
|
15
|
+
if (empty && surface.source.kind !== 'package') {
|
|
16
|
+
return [
|
|
17
|
+
{
|
|
18
|
+
ruleId: 'MTC-META-001',
|
|
19
|
+
title: 'Empty surface — nothing to analyze',
|
|
20
|
+
category: 'hygiene',
|
|
21
|
+
severity: 'info',
|
|
22
|
+
confidence: 'strong',
|
|
23
|
+
description: 'No tools, prompts, or resources were found on this surface. This may mean the server exposes ' +
|
|
24
|
+
'nothing, or that the input was not a recognizable MCP manifest. An empty surface is NOT a clean ' +
|
|
25
|
+
'bill of health — there was simply nothing to score.',
|
|
26
|
+
remediation: 'Confirm the target is the right manifest/endpoint and that the server actually advertises tools.',
|
|
27
|
+
location: { kind: 'server' },
|
|
28
|
+
},
|
|
29
|
+
];
|
|
30
|
+
}
|
|
31
|
+
return [];
|
|
32
|
+
},
|
|
33
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/*! MCP Trust Checker · https://mcptrustchecker.com · support@mcptrustchecker.com · © 2026 Illia Haidar · MIT */
|
|
2
|
+
/**
|
|
3
|
+
* Stage 6 — Transport / host posture + known-CVE version matcher.
|
|
4
|
+
* Cheap, high-value checks that content-only scanners miss.
|
|
5
|
+
*/
|
|
6
|
+
import type { Detector, Finding, PackageMeta } from '../types.js';
|
|
7
|
+
/** Match package metadata against the known-vuln table. Exported for tests. */
|
|
8
|
+
export declare function matchKnownVulns(meta: PackageMeta): Finding[];
|
|
9
|
+
export declare const postureDetector: Detector;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/*! MCP Trust Checker · https://mcptrustchecker.com · support@mcptrustchecker.com · © 2026 Illia Haidar · MIT */
|
|
2
|
+
/**
|
|
3
|
+
* Stage 6 — Transport / host posture + known-CVE version matcher.
|
|
4
|
+
* Cheap, high-value checks that content-only scanners miss.
|
|
5
|
+
*/
|
|
6
|
+
import { KNOWN_VULNS } from '../data/knownCves.js';
|
|
7
|
+
import { compareVersions } from '../util/distance.js';
|
|
8
|
+
/** Match package metadata against the known-vuln table. Exported for tests. */
|
|
9
|
+
export function matchKnownVulns(meta) {
|
|
10
|
+
const findings = [];
|
|
11
|
+
if (typeof meta.name !== 'string' || typeof meta.version !== 'string')
|
|
12
|
+
return findings;
|
|
13
|
+
for (const v of KNOWN_VULNS) {
|
|
14
|
+
if (v.package !== meta.name)
|
|
15
|
+
continue;
|
|
16
|
+
const version = meta.version;
|
|
17
|
+
// Only compare a CONCRETE version. "latest", "*", "^1.2.0", ranges etc. would
|
|
18
|
+
// otherwise parse to [0] and be flagged as vulnerable — a false CVE.
|
|
19
|
+
if (!/^v?\d/.test(version))
|
|
20
|
+
continue;
|
|
21
|
+
const vulnerable = (v.ltExclusive !== undefined && compareVersions(version, v.ltExclusive) < 0) ||
|
|
22
|
+
(v.leInclusive !== undefined && compareVersions(version, v.leInclusive) <= 0);
|
|
23
|
+
if (!vulnerable)
|
|
24
|
+
continue;
|
|
25
|
+
findings.push({
|
|
26
|
+
ruleId: 'MTC-NET-001',
|
|
27
|
+
title: `Known-vulnerable version: ${v.package}@${version} (${v.id})`,
|
|
28
|
+
category: 'supply-chain',
|
|
29
|
+
severity: v.severity,
|
|
30
|
+
confidence: 'confirmed',
|
|
31
|
+
description: `${v.package}@${version} is in the vulnerable range for ${v.id}${v.cvss ? ` (severity ${v.cvss})` : ''}: ` +
|
|
32
|
+
`${v.title}.`,
|
|
33
|
+
remediation: `Upgrade ${v.package} to a fixed release.`,
|
|
34
|
+
location: { kind: 'package', name: v.package },
|
|
35
|
+
references: [v.reference],
|
|
36
|
+
owasp: 'LLM03:2025 Supply Chain',
|
|
37
|
+
data: { cve: v.id, version },
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return findings;
|
|
41
|
+
}
|
|
42
|
+
export const postureDetector = {
|
|
43
|
+
id: 'posture',
|
|
44
|
+
stage: 6,
|
|
45
|
+
title: 'Transport & host posture',
|
|
46
|
+
run(ctx) {
|
|
47
|
+
const findings = [];
|
|
48
|
+
const t = ctx.surface.transport;
|
|
49
|
+
if (t) {
|
|
50
|
+
if (t.userControlledCommand) {
|
|
51
|
+
findings.push({
|
|
52
|
+
ruleId: 'MTC-NET-002',
|
|
53
|
+
title: 'stdio command is user/metadata-controlled without an allowlist',
|
|
54
|
+
category: 'network',
|
|
55
|
+
severity: 'critical',
|
|
56
|
+
confidence: 'confirmed',
|
|
57
|
+
description: 'The stdio transport spawns a command taken from untrusted configuration/metadata without an ' +
|
|
58
|
+
'executable allowlist. This is the systemic MCP stdio-RCE class: launching the server runs arbitrary code.',
|
|
59
|
+
remediation: 'Allowlist the executable to {npx,uvx,python,python3,node,docker,deno} and never spawn names taken verbatim from untrusted metadata.',
|
|
60
|
+
location: { kind: 'transport' },
|
|
61
|
+
owasp: 'LLM05:2025 Improper Output Handling',
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
if (t.kind === 'http' && typeof t.url === 'string' && t.url.startsWith('http://')) {
|
|
65
|
+
findings.push({
|
|
66
|
+
ruleId: 'MTC-NET-003',
|
|
67
|
+
title: 'Server reached over plaintext HTTP',
|
|
68
|
+
category: 'network',
|
|
69
|
+
severity: 'medium',
|
|
70
|
+
confidence: 'confirmed',
|
|
71
|
+
description: `The endpoint ${t.url} uses plaintext HTTP; tool traffic and any tokens are exposed on the wire.`,
|
|
72
|
+
remediation: 'Use HTTPS for all remote MCP transports.',
|
|
73
|
+
location: { kind: 'transport' },
|
|
74
|
+
owasp: 'LLM03:2025 Supply Chain',
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
if (typeof t.url === 'string') {
|
|
78
|
+
try {
|
|
79
|
+
const host = new URL(t.url).hostname;
|
|
80
|
+
const isLocal = ['localhost', '127.0.0.1', '::1'].includes(host);
|
|
81
|
+
if (isLocal) {
|
|
82
|
+
findings.push({
|
|
83
|
+
ruleId: 'MTC-NET-006',
|
|
84
|
+
title: 'Local HTTP/SSE MCP server — verify Origin validation (DNS rebinding)',
|
|
85
|
+
category: 'network',
|
|
86
|
+
severity: 'low',
|
|
87
|
+
confidence: 'heuristic',
|
|
88
|
+
description: `A browser-reachable local MCP server (${host}) is exposed to DNS-rebinding attacks unless it ` +
|
|
89
|
+
`validates the Host/Origin header — which static analysis cannot confirm.`,
|
|
90
|
+
remediation: 'Ensure the server allowlists Origin/Host, or use the SDK version that adds rebinding protection.',
|
|
91
|
+
location: { kind: 'transport' },
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
if (host === '0.0.0.0') {
|
|
95
|
+
findings.push({
|
|
96
|
+
ruleId: 'MTC-NET-004',
|
|
97
|
+
title: 'Server bound to 0.0.0.0',
|
|
98
|
+
category: 'network',
|
|
99
|
+
severity: 'medium',
|
|
100
|
+
confidence: 'strong',
|
|
101
|
+
description: 'Binding to 0.0.0.0 exposes the MCP server on all interfaces; ensure authentication is enforced.',
|
|
102
|
+
remediation: 'Bind to localhost for local servers, or require auth for remote exposure.',
|
|
103
|
+
location: { kind: 'transport' },
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
else if (!isLocal) {
|
|
107
|
+
// Remote host — worth a hygiene note about auth being out-of-scope for static analysis.
|
|
108
|
+
findings.push({
|
|
109
|
+
ruleId: 'MTC-NET-005',
|
|
110
|
+
title: 'Remote HTTP MCP endpoint',
|
|
111
|
+
category: 'network',
|
|
112
|
+
severity: 'info',
|
|
113
|
+
confidence: 'strong',
|
|
114
|
+
description: `Remote endpoint ${host}. MCP Trust Checker does not test server-side authentication/authorization; verify the endpoint requires auth.`,
|
|
115
|
+
location: { kind: 'transport' },
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
/* not a parseable URL — ignore */
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (ctx.surface.packageMeta)
|
|
125
|
+
findings.push(...matchKnownVulns(ctx.surface.packageMeta));
|
|
126
|
+
return findings;
|
|
127
|
+
},
|
|
128
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/*! MCP Trust Checker · https://mcptrustchecker.com · support@mcptrustchecker.com · © 2026 Illia Haidar · MIT */
|
|
2
|
+
/**
|
|
3
|
+
* Stage 4b — Implementation-level analysis.
|
|
4
|
+
*
|
|
5
|
+
* Metadata detectors read what a server *claims*. This one reads what its code
|
|
6
|
+
* *does*: when the server's source is available (a local package directory or an
|
|
7
|
+
* extracted tarball), it scans for dangerous sinks — arbitrary execution, shell
|
|
8
|
+
* spawning, hardcoded egress, obfuscated payloads, credential reads. Fully
|
|
9
|
+
* deterministic; no LLM, no execution of the code.
|
|
10
|
+
*/
|
|
11
|
+
import type { Detector } from '../types.js';
|
|
12
|
+
export declare const sourceDetector: Detector;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/*! MCP Trust Checker · https://mcptrustchecker.com · support@mcptrustchecker.com · © 2026 Illia Haidar · MIT */
|
|
2
|
+
/**
|
|
3
|
+
* Stage 4b — Implementation-level analysis.
|
|
4
|
+
*
|
|
5
|
+
* Metadata detectors read what a server *claims*. This one reads what its code
|
|
6
|
+
* *does*: when the server's source is available (a local package directory or an
|
|
7
|
+
* extracted tarball), it scans for dangerous sinks — arbitrary execution, shell
|
|
8
|
+
* spawning, hardcoded egress, obfuscated payloads, credential reads. Fully
|
|
9
|
+
* deterministic; no LLM, no execution of the code.
|
|
10
|
+
*/
|
|
11
|
+
import { SOURCE_PATTERNS } from '../data/sourcePatterns.js';
|
|
12
|
+
import { SECRET_PATTERNS } from '../data/injectionPatterns.js';
|
|
13
|
+
const MAX_FINDINGS_PER_RULE = 10; // don't drown the report on a big codebase
|
|
14
|
+
function lineOf(content, index) {
|
|
15
|
+
let line = 1;
|
|
16
|
+
for (let i = 0; i < index && i < content.length; i++)
|
|
17
|
+
if (content[i] === '\n')
|
|
18
|
+
line += 1;
|
|
19
|
+
return line;
|
|
20
|
+
}
|
|
21
|
+
export const sourceDetector = {
|
|
22
|
+
id: 'source',
|
|
23
|
+
stage: 4,
|
|
24
|
+
title: 'Implementation-level source analysis',
|
|
25
|
+
run(ctx) {
|
|
26
|
+
const files = Array.isArray(ctx.surface.sourceFiles) ? ctx.surface.sourceFiles : [];
|
|
27
|
+
if (!files.length)
|
|
28
|
+
return [];
|
|
29
|
+
const findings = [];
|
|
30
|
+
const perRule = new Map();
|
|
31
|
+
for (const f of files) {
|
|
32
|
+
if (!f || typeof f.content !== 'string')
|
|
33
|
+
continue;
|
|
34
|
+
const path = typeof f.path === 'string' ? f.path : 'source';
|
|
35
|
+
for (const p of SOURCE_PATTERNS) {
|
|
36
|
+
const seen = perRule.get(p.id) ?? 0;
|
|
37
|
+
if (seen >= MAX_FINDINGS_PER_RULE)
|
|
38
|
+
continue;
|
|
39
|
+
const m = f.content.match(p.pattern);
|
|
40
|
+
if (!m || m.index === undefined)
|
|
41
|
+
continue;
|
|
42
|
+
perRule.set(p.id, seen + 1);
|
|
43
|
+
findings.push({
|
|
44
|
+
ruleId: p.id,
|
|
45
|
+
title: `${p.title} (${path})`,
|
|
46
|
+
category: p.category,
|
|
47
|
+
severity: p.severity,
|
|
48
|
+
confidence: p.confidence,
|
|
49
|
+
description: `In the server's implementation (\`${path}:${lineOf(f.content, m.index)}\`): ${p.why} ` +
|
|
50
|
+
`This is read from the code itself — not from the tool description — so a poisoned server cannot hide it behind honest-looking metadata.`,
|
|
51
|
+
remediation: 'Review this call path: confirm it never receives unsanitized tool input, constrain it, or remove it. ' +
|
|
52
|
+
'Treat a server whose code reaches these sinks as high-capability regardless of what its tools claim.',
|
|
53
|
+
location: { kind: 'server', name: path },
|
|
54
|
+
evidence: m[0].slice(0, 160),
|
|
55
|
+
owasp: p.category === 'exfiltration' ? 'LLM02:2025 Sensitive Information Disclosure' : 'LLM05:2025 Improper Output Handling',
|
|
56
|
+
data: { rule: p.id, file: path, line: lineOf(f.content, m.index) },
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
// Embedded credential VALUE hardcoded in source (not just referenced).
|
|
60
|
+
const seenSecret = perRule.get('MTC-SRC-008') ?? 0;
|
|
61
|
+
if (seenSecret < MAX_FINDINGS_PER_RULE) {
|
|
62
|
+
for (const sp of SECRET_PATTERNS) {
|
|
63
|
+
const m = f.content.match(sp.pattern);
|
|
64
|
+
if (!m || m.index === undefined)
|
|
65
|
+
continue;
|
|
66
|
+
perRule.set('MTC-SRC-008', seenSecret + 1);
|
|
67
|
+
findings.push({
|
|
68
|
+
ruleId: 'MTC-SRC-008',
|
|
69
|
+
title: `Hardcoded ${sp.label} in server code (${path})`,
|
|
70
|
+
category: 'exfiltration',
|
|
71
|
+
severity: 'high',
|
|
72
|
+
confidence: 'confirmed',
|
|
73
|
+
description: `A live-looking ${sp.label} is hardcoded in \`${path}:${lineOf(f.content, m.index)}\`. Secrets in source ` +
|
|
74
|
+
`ship to everyone who installs the package and are a direct credential leak.`,
|
|
75
|
+
remediation: 'Remove the secret, rotate it, and load credentials from the environment or a secret store.',
|
|
76
|
+
location: { kind: 'server', name: path },
|
|
77
|
+
evidence: `${sp.label}: ${m[0].slice(0, 4)}…(redacted)`,
|
|
78
|
+
owasp: 'LLM02:2025 Sensitive Information Disclosure',
|
|
79
|
+
data: { secretType: sp.id, file: path, line: lineOf(f.content, m.index) },
|
|
80
|
+
});
|
|
81
|
+
break; // one secret finding per file is enough
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return findings;
|
|
86
|
+
},
|
|
87
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/*! MCP Trust Checker · https://mcptrustchecker.com · support@mcptrustchecker.com · © 2026 Illia Haidar · MIT */
|
|
2
|
+
/**
|
|
3
|
+
* Stage 5 — Supply-chain & typosquatting.
|
|
4
|
+
*
|
|
5
|
+
* Multi-signal, not a single edit-distance number: known-squat table, fake
|
|
6
|
+
* official scope, unscoped shadowing, confusable skeleton, Damerau-Levenshtein
|
|
7
|
+
* gated by a download anomaly, combosquat suffix stripping, plus install-script
|
|
8
|
+
* / provenance risk. Anchored to the curated protected list (never all-pairs).
|
|
9
|
+
*/
|
|
10
|
+
import type { Detector, Finding, PackageMeta, ResolvedConfig } from '../types.js';
|
|
11
|
+
/** Pure typosquat analysis for a single package name. Exported for tests. */
|
|
12
|
+
export declare function analyzeTyposquat(name: string, meta: PackageMeta | undefined, config: ResolvedConfig): Finding[];
|
|
13
|
+
/** Install-script & provenance risk from package metadata. */
|
|
14
|
+
export declare function analyzeProvenance(meta: PackageMeta): Finding[];
|
|
15
|
+
/** Check a package's declared dependencies for squats / known-vuln names. */
|
|
16
|
+
export declare function analyzeDependencies(meta: PackageMeta, config: ResolvedConfig): Finding[];
|
|
17
|
+
export declare const supplyChainDetector: Detector;
|