mcp-security-lab 0.2.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 +17 -0
- package/README.md +175 -0
- package/SECURITY.md +25 -0
- package/THREAT_MODEL.md +66 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +113 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/config.d.ts +2 -0
- package/dist/src/config.js +90 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/environment.d.ts +12 -0
- package/dist/src/environment.js +145 -0
- package/dist/src/environment.js.map +1 -0
- package/dist/src/reporter.d.ts +4 -0
- package/dist/src/reporter.js +161 -0
- package/dist/src/reporter.js.map +1 -0
- package/dist/src/rules/injection.d.ts +16 -0
- package/dist/src/rules/injection.js +91 -0
- package/dist/src/rules/injection.js.map +1 -0
- package/dist/src/rules/launch.d.ts +2 -0
- package/dist/src/rules/launch.js +102 -0
- package/dist/src/rules/launch.js.map +1 -0
- package/dist/src/rules/oauth.d.ts +21 -0
- package/dist/src/rules/oauth.js +297 -0
- package/dist/src/rules/oauth.js.map +1 -0
- package/dist/src/rules/remote.d.ts +12 -0
- package/dist/src/rules/remote.js +81 -0
- package/dist/src/rules/remote.js.map +1 -0
- package/dist/src/rules/tools.d.ts +9 -0
- package/dist/src/rules/tools.js +216 -0
- package/dist/src/rules/tools.js.map +1 -0
- package/dist/src/scanner.d.ts +2 -0
- package/dist/src/scanner.js +393 -0
- package/dist/src/scanner.js.map +1 -0
- package/dist/src/types.d.ts +72 -0
- package/dist/src/types.js +2 -0
- package/dist/src/types.js.map +1 -0
- package/docs/RULES.md +108 -0
- package/package.json +93 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
const SAFE_ENV_KEYS = [
|
|
2
|
+
"COMSPEC",
|
|
3
|
+
"OS",
|
|
4
|
+
"PATH",
|
|
5
|
+
"PATHEXT",
|
|
6
|
+
"SYSTEMDRIVE",
|
|
7
|
+
"SYSTEMROOT",
|
|
8
|
+
"TEMP",
|
|
9
|
+
"TMP",
|
|
10
|
+
"WINDIR",
|
|
11
|
+
"HOME",
|
|
12
|
+
"LANG",
|
|
13
|
+
"LC_ALL",
|
|
14
|
+
"SHELL",
|
|
15
|
+
];
|
|
16
|
+
const NON_OVERRIDABLE_ENV_KEYS = [
|
|
17
|
+
"DYLD_INSERT_LIBRARIES",
|
|
18
|
+
"DYLD_LIBRARY_PATH",
|
|
19
|
+
"LD_LIBRARY_PATH",
|
|
20
|
+
"LD_PRELOAD",
|
|
21
|
+
"MCP_SECURITY_LAB",
|
|
22
|
+
"NODE_OPTIONS",
|
|
23
|
+
"NODE_PATH",
|
|
24
|
+
];
|
|
25
|
+
export const RESERVED_ENV_KEYS = new Set([
|
|
26
|
+
...SAFE_ENV_KEYS,
|
|
27
|
+
...NON_OVERRIDABLE_ENV_KEYS,
|
|
28
|
+
]);
|
|
29
|
+
const SENSITIVE_FLAG_PATTERN = /^--?(?:api[-_]?key|authorization|credential|password|secret|token)$/i;
|
|
30
|
+
const SENSITIVE_ASSIGNMENT_PATTERN = /^((?:--?)?(?:api[-_]?key|authorization|credential|password|secret|token)=).+$/i;
|
|
31
|
+
const SENSITIVE_ENV_ASSIGNMENT_PATTERN = /^([A-Z0-9_]*(?:API_KEY|AUTHORIZATION|CREDENTIAL|PASSWORD|SECRET|TOKEN)=).+$/i;
|
|
32
|
+
const URL_CREDENTIAL_PATTERN = /^([a-z][a-z0-9+.-]*:\/\/)[^/@\s]+@/i;
|
|
33
|
+
export function createSanitizedEnvironment(source = process.env) {
|
|
34
|
+
const environment = {};
|
|
35
|
+
for (const key of SAFE_ENV_KEYS) {
|
|
36
|
+
const value = source[key];
|
|
37
|
+
if (value !== undefined) {
|
|
38
|
+
environment[key] = value;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
environment.MCP_SECURITY_LAB = "1";
|
|
42
|
+
return environment;
|
|
43
|
+
}
|
|
44
|
+
function normalizeKey(key) {
|
|
45
|
+
return key.toLowerCase().replace(/[-_]/g, "");
|
|
46
|
+
}
|
|
47
|
+
// Compound names that are unambiguously credentials. Matched as substrings so
|
|
48
|
+
// client_secret, refresh_token, id_token, X-Amz-Security-Token, X-Amz-Signature,
|
|
49
|
+
// api_key, etc. are all caught.
|
|
50
|
+
const CREDENTIAL_NEEDLES = [
|
|
51
|
+
"secret",
|
|
52
|
+
"password",
|
|
53
|
+
"passwd",
|
|
54
|
+
"credential",
|
|
55
|
+
"signature",
|
|
56
|
+
"apikey",
|
|
57
|
+
"accesskey",
|
|
58
|
+
"privatekey",
|
|
59
|
+
"accesstoken",
|
|
60
|
+
"refreshtoken",
|
|
61
|
+
"idtoken",
|
|
62
|
+
"securitytoken",
|
|
63
|
+
"sessiontoken",
|
|
64
|
+
"bearer",
|
|
65
|
+
];
|
|
66
|
+
// Additional bare keys redacted from reports (over-redaction is safe there) but
|
|
67
|
+
// NOT treated as findings, since a bare ?token=/?key= is often an opaque cursor.
|
|
68
|
+
const BROAD_REDACTION_KEYS = new Set(["token", "key", "auth", "authorization", "sig", "pwd"]);
|
|
69
|
+
/** Strong, low-false-positive test used to RAISE a credentials-in-URL finding. */
|
|
70
|
+
export function isCredentialQueryKey(key) {
|
|
71
|
+
const normalized = normalizeKey(key);
|
|
72
|
+
return CREDENTIAL_NEEDLES.some((needle) => normalized.includes(needle));
|
|
73
|
+
}
|
|
74
|
+
/** Broad test used only to REDACT report output, where over-redaction is safe. */
|
|
75
|
+
function isRedactableQueryKey(key) {
|
|
76
|
+
return isCredentialQueryKey(key) || BROAD_REDACTION_KEYS.has(normalizeKey(key));
|
|
77
|
+
}
|
|
78
|
+
function redactParams(params) {
|
|
79
|
+
let changed = false;
|
|
80
|
+
for (const key of [...params.keys()]) {
|
|
81
|
+
if (isRedactableQueryKey(key)) {
|
|
82
|
+
params.set(key, "[REDACTED]");
|
|
83
|
+
changed = true;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return changed;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Remove credentials from a URL before it enters a report: strip any userinfo
|
|
90
|
+
* (`user:pass@`) and redact the value of sensitive query AND fragment parameters
|
|
91
|
+
* (OAuth implicit-flow tokens live in the fragment). Falls back to a coarse regex
|
|
92
|
+
* when the string is not a parseable URL.
|
|
93
|
+
*/
|
|
94
|
+
export function redactUrl(rawUrl) {
|
|
95
|
+
try {
|
|
96
|
+
const url = new URL(rawUrl);
|
|
97
|
+
if (url.username !== "" || url.password !== "") {
|
|
98
|
+
url.username = "[REDACTED]";
|
|
99
|
+
url.password = "";
|
|
100
|
+
}
|
|
101
|
+
redactParams(url.searchParams);
|
|
102
|
+
if (url.hash.length > 1) {
|
|
103
|
+
const fragment = new URLSearchParams(url.hash.slice(1));
|
|
104
|
+
if (redactParams(fragment)) {
|
|
105
|
+
url.hash = `#${fragment.toString()}`;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// URL percent-encodes the brackets of our sentinel; restore it for readability.
|
|
109
|
+
return url.toString().replace(/%5BREDACTED%5D/g, "[REDACTED]");
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return rawUrl.replace(URL_CREDENTIAL_PATTERN, "$1[REDACTED]@");
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
export function redactArguments(args) {
|
|
116
|
+
const redacted = [];
|
|
117
|
+
let redactNext = false;
|
|
118
|
+
for (const argument of args) {
|
|
119
|
+
if (redactNext) {
|
|
120
|
+
redacted.push("[REDACTED]");
|
|
121
|
+
redactNext = false;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (SENSITIVE_FLAG_PATTERN.test(argument)) {
|
|
125
|
+
redacted.push(argument);
|
|
126
|
+
redactNext = true;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (SENSITIVE_ASSIGNMENT_PATTERN.test(argument)) {
|
|
130
|
+
redacted.push(argument.replace(SENSITIVE_ASSIGNMENT_PATTERN, "$1[REDACTED]"));
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (SENSITIVE_ENV_ASSIGNMENT_PATTERN.test(argument)) {
|
|
134
|
+
redacted.push(argument.replace(SENSITIVE_ENV_ASSIGNMENT_PATTERN, "$1[REDACTED]"));
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (URL_CREDENTIAL_PATTERN.test(argument)) {
|
|
138
|
+
redacted.push(argument.replace(URL_CREDENTIAL_PATTERN, "$1[REDACTED]@"));
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
redacted.push(argument);
|
|
142
|
+
}
|
|
143
|
+
return redacted;
|
|
144
|
+
}
|
|
145
|
+
//# sourceMappingURL=environment.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"environment.js","sourceRoot":"","sources":["../../src/environment.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ScanReport } from "./types.js";
|
|
2
|
+
export declare function reportAsJson(report: ScanReport): string;
|
|
3
|
+
export declare function reportAsSarif(report: ScanReport, configPath: string, workspaceRoot?: string): string;
|
|
4
|
+
export declare function reportAsText(report: ScanReport): string;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { basename, isAbsolute, relative, resolve, sep } from "node:path";
|
|
3
|
+
const ICONS = {
|
|
4
|
+
critical: "CRITICAL",
|
|
5
|
+
high: "HIGH",
|
|
6
|
+
medium: "MEDIUM",
|
|
7
|
+
low: "LOW",
|
|
8
|
+
info: "INFO",
|
|
9
|
+
};
|
|
10
|
+
export function reportAsJson(report) {
|
|
11
|
+
return `${JSON.stringify(report, null, 2)}\n`;
|
|
12
|
+
}
|
|
13
|
+
function sarifLevel(severity) {
|
|
14
|
+
if (severity === "critical" || severity === "high") {
|
|
15
|
+
return "error";
|
|
16
|
+
}
|
|
17
|
+
if (severity === "medium") {
|
|
18
|
+
return "warning";
|
|
19
|
+
}
|
|
20
|
+
if (severity === "low") {
|
|
21
|
+
return "note";
|
|
22
|
+
}
|
|
23
|
+
return "none";
|
|
24
|
+
}
|
|
25
|
+
function artifactUri(configPath, workspaceRoot) {
|
|
26
|
+
const absoluteConfigPath = resolve(configPath);
|
|
27
|
+
const absoluteWorkspaceRoot = resolve(workspaceRoot);
|
|
28
|
+
const relativePath = relative(absoluteWorkspaceRoot, absoluteConfigPath);
|
|
29
|
+
const outsideWorkspace = relativePath === ".." || relativePath.startsWith(`..${sep}`) || isAbsolute(relativePath);
|
|
30
|
+
return (outsideWorkspace ? basename(absoluteConfigPath) : relativePath || basename(absoluteConfigPath))
|
|
31
|
+
.split(sep)
|
|
32
|
+
.join("/");
|
|
33
|
+
}
|
|
34
|
+
function fingerprint(id, location, title, evidence) {
|
|
35
|
+
return createHash("sha256")
|
|
36
|
+
.update([id, location ?? "", title, evidence].join("\u0000"))
|
|
37
|
+
.digest("hex");
|
|
38
|
+
}
|
|
39
|
+
export function reportAsSarif(report, configPath, workspaceRoot = process.env.GITHUB_WORKSPACE ?? process.cwd()) {
|
|
40
|
+
const rules = new Map();
|
|
41
|
+
const uri = artifactUri(configPath, workspaceRoot);
|
|
42
|
+
for (const finding of report.findings) {
|
|
43
|
+
if (!rules.has(finding.id)) {
|
|
44
|
+
const tags = ["security", "mcp"];
|
|
45
|
+
if (finding.cwe !== undefined) {
|
|
46
|
+
tags.push(`external/cwe/${finding.cwe.toLowerCase()}`);
|
|
47
|
+
}
|
|
48
|
+
if (finding.owasp !== undefined) {
|
|
49
|
+
tags.push(`external/owasp-llm/${finding.owasp.toLowerCase()}`);
|
|
50
|
+
}
|
|
51
|
+
const taxonomy = [
|
|
52
|
+
finding.cwe === undefined ? undefined : `CWE: ${finding.cwe}`,
|
|
53
|
+
finding.owasp === undefined ? undefined : `OWASP LLM: ${finding.owasp}`,
|
|
54
|
+
]
|
|
55
|
+
.filter((entry) => entry !== undefined)
|
|
56
|
+
.join(" | ");
|
|
57
|
+
rules.set(finding.id, {
|
|
58
|
+
id: finding.id,
|
|
59
|
+
shortDescription: { text: finding.title },
|
|
60
|
+
fullDescription: { text: finding.recommendation },
|
|
61
|
+
defaultConfiguration: { level: sarifLevel(finding.severity) },
|
|
62
|
+
help: {
|
|
63
|
+
text: finding.recommendation,
|
|
64
|
+
markdown: `**Recommendation:** ${finding.recommendation}${taxonomy === "" ? "" : `\n\n**Taxonomy:** ${taxonomy}`}`,
|
|
65
|
+
},
|
|
66
|
+
properties: {
|
|
67
|
+
tags,
|
|
68
|
+
...(finding.cwe === undefined ? {} : { cwe: finding.cwe }),
|
|
69
|
+
...(finding.owasp === undefined ? {} : { owasp: finding.owasp }),
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const sarif = {
|
|
75
|
+
$schema: "https://json.schemastore.org/sarif-2.1.0.json",
|
|
76
|
+
version: "2.1.0",
|
|
77
|
+
runs: [
|
|
78
|
+
{
|
|
79
|
+
tool: {
|
|
80
|
+
driver: {
|
|
81
|
+
name: report.scanner.name,
|
|
82
|
+
semanticVersion: report.scanner.version,
|
|
83
|
+
rules: [...rules.values()],
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
automationDetails: {
|
|
87
|
+
id: "mcp-security-lab/",
|
|
88
|
+
},
|
|
89
|
+
invocations: [
|
|
90
|
+
{
|
|
91
|
+
executionSuccessful: true,
|
|
92
|
+
properties: {
|
|
93
|
+
connected: report.execution.connected,
|
|
94
|
+
toolsInvoked: report.execution.toolsInvoked,
|
|
95
|
+
osSandboxed: report.execution.osSandboxed,
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
results: report.findings.map((finding) => ({
|
|
100
|
+
ruleId: finding.id,
|
|
101
|
+
level: sarifLevel(finding.severity),
|
|
102
|
+
message: {
|
|
103
|
+
text: `${finding.title}\n\nEvidence: ${finding.evidence}\n\nRecommendation: ${finding.recommendation}`,
|
|
104
|
+
},
|
|
105
|
+
locations: [
|
|
106
|
+
{
|
|
107
|
+
physicalLocation: {
|
|
108
|
+
artifactLocation: { uri },
|
|
109
|
+
region: { startLine: 1 },
|
|
110
|
+
},
|
|
111
|
+
logicalLocations: finding.location === undefined
|
|
112
|
+
? []
|
|
113
|
+
: [{ fullyQualifiedName: finding.location, kind: "configuration" }],
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
partialFingerprints: {
|
|
117
|
+
"mcpSecurityLab/v1": fingerprint(finding.id, finding.location, finding.title, finding.evidence),
|
|
118
|
+
},
|
|
119
|
+
properties: {
|
|
120
|
+
severity: finding.severity,
|
|
121
|
+
recommendation: finding.recommendation,
|
|
122
|
+
scannerLocation: finding.location ?? "scan",
|
|
123
|
+
...(finding.cwe === undefined ? {} : { cwe: finding.cwe }),
|
|
124
|
+
...(finding.owasp === undefined ? {} : { owasp: finding.owasp }),
|
|
125
|
+
},
|
|
126
|
+
})),
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
};
|
|
130
|
+
return `${JSON.stringify(sarif, null, 2)}\n`;
|
|
131
|
+
}
|
|
132
|
+
export function reportAsText(report) {
|
|
133
|
+
const targetLabel = report.target.url !== undefined
|
|
134
|
+
? report.target.url
|
|
135
|
+
: `${report.target.command} ${(report.target.args ?? []).join(" ")}`.trim();
|
|
136
|
+
const lines = [
|
|
137
|
+
"MCP Security Lab",
|
|
138
|
+
`Target: ${targetLabel}`,
|
|
139
|
+
`Connected: ${report.execution.connected ? "yes" : "no"} | Transport: ${report.execution.transport} | Tools invoked: ${report.execution.toolsInvoked} | OS sandbox: ${report.execution.osSandboxed ? "yes" : "no"}`,
|
|
140
|
+
`Findings: ${report.summary.critical} critical, ${report.summary.high} high, ${report.summary.medium} medium, ${report.summary.low} low, ${report.summary.info} info`,
|
|
141
|
+
"",
|
|
142
|
+
];
|
|
143
|
+
for (const finding of report.findings) {
|
|
144
|
+
lines.push(`[${ICONS[finding.severity]}] ${finding.id} ${finding.title}`);
|
|
145
|
+
lines.push(` Evidence: ${finding.evidence}`);
|
|
146
|
+
lines.push(` Recommendation: ${finding.recommendation}`);
|
|
147
|
+
const taxonomy = [finding.cwe, finding.owasp].filter(Boolean).join(", ");
|
|
148
|
+
if (taxonomy !== "") {
|
|
149
|
+
lines.push(` Taxonomy: ${taxonomy}`);
|
|
150
|
+
}
|
|
151
|
+
if (finding.location !== undefined) {
|
|
152
|
+
lines.push(` Location: ${finding.location}`);
|
|
153
|
+
}
|
|
154
|
+
lines.push("");
|
|
155
|
+
}
|
|
156
|
+
lines.push(report.execution.osSandboxed
|
|
157
|
+
? "Note: target ran inside a network-isolated Docker container."
|
|
158
|
+
: "Limitation: target processes are not isolated from the host filesystem or network.");
|
|
159
|
+
return `${lines.join("\n")}\n`;
|
|
160
|
+
}
|
|
161
|
+
//# sourceMappingURL=reporter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reporter.js","sourceRoot":"","sources":["../../src/reporter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Finding } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Normalize untrusted text before pattern matching. NFKC folds homoglyph and
|
|
4
|
+
* fullwidth variants onto their canonical form so that, for example, fullwidth
|
|
5
|
+
* "ignore" collapses to "ignore"; invisible characters are then stripped so
|
|
6
|
+
* they cannot break up a keyword mid-word.
|
|
7
|
+
*/
|
|
8
|
+
export declare function normalizeText(text: string): string;
|
|
9
|
+
export declare function hasInvisibleCharacters(text: string): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Scan a single untrusted string for prompt-injection-like instructions.
|
|
12
|
+
* Matching runs against the normalized form so evasion via Unicode tricks is
|
|
13
|
+
* defeated. `sourceLabel` names where the text came from (e.g. "description",
|
|
14
|
+
* "parameter data.query") for actionable evidence.
|
|
15
|
+
*/
|
|
16
|
+
export declare function scanTextForInjection(text: string, location: string, sourceLabel: string): Finding[];
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
const PROMPT_INJECTION_PATTERNS = [
|
|
2
|
+
{
|
|
3
|
+
pattern: /\bignore (?:all |any )?(?:previous|prior) instructions?\b/i,
|
|
4
|
+
label: "instruction override",
|
|
5
|
+
},
|
|
6
|
+
{
|
|
7
|
+
pattern: /\b(?:always|must) (?:call|invoke|use) (?:this|the) tool\b/i,
|
|
8
|
+
label: "forced invocation",
|
|
9
|
+
},
|
|
10
|
+
{ pattern: /\bdo not (?:tell|inform|show) the user\b/i, label: "concealment" },
|
|
11
|
+
{ pattern: /\b(?:system prompt|hidden instructions?)\b/i, label: "hidden prompt reference" },
|
|
12
|
+
{ pattern: /\bdisable|bypass\b.{0,30}\b(?:safety|security|permission)/i, label: "safety bypass" },
|
|
13
|
+
];
|
|
14
|
+
// Characters an author never needs in a human-readable description but that an
|
|
15
|
+
// attacker uses to smuggle instructions past a naive substring scan. Built from
|
|
16
|
+
// explicit code-point ranges so this source file itself stays free of invisible
|
|
17
|
+
// characters and remains reviewable.
|
|
18
|
+
// C0 controls (keep \t=0x09, \n=0x0A, \r=0x0D), DEL + C1 controls
|
|
19
|
+
// soft hyphen, zero-width space..RLM, bidi overrides, word joiner..BOM
|
|
20
|
+
const INVISIBLE_CODEPOINT_RANGES = [
|
|
21
|
+
[0x0, 0x8],
|
|
22
|
+
[0xb, 0xc],
|
|
23
|
+
[0xe, 0x1f],
|
|
24
|
+
[0x7f, 0x9f],
|
|
25
|
+
[0xad, 0xad], // soft hyphen
|
|
26
|
+
[0x200b, 0x200f], // zero-width space, ZWNJ, ZWJ, LRM, RLM
|
|
27
|
+
[0x202a, 0x202e], // bidirectional embedding/override controls
|
|
28
|
+
[0x2060, 0x2064], // word joiner, invisible operators
|
|
29
|
+
[0xfeff, 0xfeff], // zero-width no-break space / BOM
|
|
30
|
+
];
|
|
31
|
+
function buildInvisibleCharPattern(flags) {
|
|
32
|
+
const body = INVISIBLE_CODEPOINT_RANGES.map(([start, end]) => start === end
|
|
33
|
+
? `\\u{${start.toString(16)}}`
|
|
34
|
+
: `\\u{${start.toString(16)}}-\\u{${end.toString(16)}}`).join("");
|
|
35
|
+
return new RegExp(`[${body}]`, flags);
|
|
36
|
+
}
|
|
37
|
+
const INVISIBLE_CHAR_TEST = buildInvisibleCharPattern("u");
|
|
38
|
+
const INVISIBLE_CHAR_STRIP = buildInvisibleCharPattern("gu");
|
|
39
|
+
/**
|
|
40
|
+
* Normalize untrusted text before pattern matching. NFKC folds homoglyph and
|
|
41
|
+
* fullwidth variants onto their canonical form so that, for example, fullwidth
|
|
42
|
+
* "ignore" collapses to "ignore"; invisible characters are then stripped so
|
|
43
|
+
* they cannot break up a keyword mid-word.
|
|
44
|
+
*/
|
|
45
|
+
export function normalizeText(text) {
|
|
46
|
+
return text.normalize("NFKC").replace(INVISIBLE_CHAR_STRIP, "");
|
|
47
|
+
}
|
|
48
|
+
export function hasInvisibleCharacters(text) {
|
|
49
|
+
return INVISIBLE_CHAR_TEST.test(text);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Scan a single untrusted string for prompt-injection-like instructions.
|
|
53
|
+
* Matching runs against the normalized form so evasion via Unicode tricks is
|
|
54
|
+
* defeated. `sourceLabel` names where the text came from (e.g. "description",
|
|
55
|
+
* "parameter data.query") for actionable evidence.
|
|
56
|
+
*/
|
|
57
|
+
export function scanTextForInjection(text, location, sourceLabel) {
|
|
58
|
+
const findings = [];
|
|
59
|
+
if (text.trim() === "") {
|
|
60
|
+
return findings;
|
|
61
|
+
}
|
|
62
|
+
if (hasInvisibleCharacters(text)) {
|
|
63
|
+
findings.push({
|
|
64
|
+
id: "TOOL012",
|
|
65
|
+
severity: "high",
|
|
66
|
+
title: "Text contains invisible or control characters",
|
|
67
|
+
evidence: `The ${sourceLabel} contains zero-width, bidirectional, or control characters that are invisible to a human reviewer.`,
|
|
68
|
+
recommendation: "Remove invisible and control characters; server-injected text must be exactly what a human reviewer sees.",
|
|
69
|
+
location,
|
|
70
|
+
cwe: "CWE-1007",
|
|
71
|
+
owasp: "LLM01",
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
const normalized = normalizeText(text);
|
|
75
|
+
for (const candidate of PROMPT_INJECTION_PATTERNS) {
|
|
76
|
+
if (candidate.pattern.test(normalized)) {
|
|
77
|
+
findings.push({
|
|
78
|
+
id: "TOOL003",
|
|
79
|
+
severity: "high",
|
|
80
|
+
title: "Text contains a prompt-injection-like instruction",
|
|
81
|
+
evidence: `The ${sourceLabel} matched the ${candidate.label} pattern.`,
|
|
82
|
+
recommendation: "Describe functionality only; remove behavioral or hidden instructions.",
|
|
83
|
+
location,
|
|
84
|
+
cwe: "CWE-77",
|
|
85
|
+
owasp: "LLM01",
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return findings;
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=injection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"injection.js","sourceRoot":"","sources":["../../../src/rules/injection.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
const SHELL_NAMES = new Set([
|
|
3
|
+
"bash",
|
|
4
|
+
"bash.exe",
|
|
5
|
+
"cmd",
|
|
6
|
+
"cmd.exe",
|
|
7
|
+
"pwsh",
|
|
8
|
+
"pwsh.exe",
|
|
9
|
+
"powershell",
|
|
10
|
+
"powershell.exe",
|
|
11
|
+
"sh",
|
|
12
|
+
"sh.exe",
|
|
13
|
+
]);
|
|
14
|
+
const NETWORK_INSTALLERS = new Set(["npx", "npx.cmd", "pipx", "uvx"]);
|
|
15
|
+
const SUSPICIOUS_ARGUMENTS = [
|
|
16
|
+
{ pattern: /\brm\s+-rf\b/i, label: "recursive deletion" },
|
|
17
|
+
{ pattern: /\bremove-item\b.*\b-recurse\b/i, label: "recursive deletion" },
|
|
18
|
+
{ pattern: /\bcurl\b|\bwget\b|invoke-webrequest/i, label: "network download" },
|
|
19
|
+
{ pattern: /(?:^|[;&|])\s*(?:cmd|powershell|pwsh|bash|sh)\b/i, label: "nested shell" },
|
|
20
|
+
{ pattern: /\b(?:sudo|runas)\b/i, label: "privilege escalation" },
|
|
21
|
+
{ pattern: /\b(?:iex|invoke-expression)\b/i, label: "dynamic code execution" },
|
|
22
|
+
{ pattern: /\bfrombase64string\b|base64\s+-d\b|base64\s+--decode\b/i, label: "encoded payload" },
|
|
23
|
+
];
|
|
24
|
+
// Interpreters paired with the flags that make them execute inline source
|
|
25
|
+
// supplied on the command line, bypassing any reviewable script file.
|
|
26
|
+
const INLINE_EVAL_RULES = [
|
|
27
|
+
{
|
|
28
|
+
executables: new Set(["node", "node.exe", "deno", "deno.exe", "bun", "bun.exe"]),
|
|
29
|
+
flags: /^(?:-e|--eval|-p|--print|eval)$/i,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
executables: new Set(["python", "python.exe", "python3", "python3.exe"]),
|
|
33
|
+
flags: /^-c$/,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
executables: new Set(["ruby", "ruby.exe", "perl", "perl.exe"]),
|
|
37
|
+
flags: /^-e$/,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
executables: new Set(["pwsh", "pwsh.exe", "powershell", "powershell.exe"]),
|
|
41
|
+
flags: /^-(?:e|ec|enc|encodedcommand|command|c)$/i,
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
export function inspectLaunch(target) {
|
|
45
|
+
const findings = [];
|
|
46
|
+
if (target.command === undefined || target.args === undefined) {
|
|
47
|
+
return findings;
|
|
48
|
+
}
|
|
49
|
+
const executable = basename(target.command).toLowerCase();
|
|
50
|
+
const joinedArguments = target.args.join(" ");
|
|
51
|
+
if (SHELL_NAMES.has(executable)) {
|
|
52
|
+
findings.push({
|
|
53
|
+
id: "LAUNCH001",
|
|
54
|
+
severity: "high",
|
|
55
|
+
title: "Target is launched through a general-purpose shell",
|
|
56
|
+
evidence: `Configured executable is "${executable}".`,
|
|
57
|
+
recommendation: "Launch a fixed executable directly and pass arguments as an array.",
|
|
58
|
+
location: "target.command",
|
|
59
|
+
cwe: "CWE-78",
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
if (NETWORK_INSTALLERS.has(executable)) {
|
|
63
|
+
findings.push({
|
|
64
|
+
id: "LAUNCH002",
|
|
65
|
+
severity: "medium",
|
|
66
|
+
title: "Target may download executable code at startup",
|
|
67
|
+
evidence: `Configured executable is "${executable}".`,
|
|
68
|
+
recommendation: "Pin and install the package before scanning, then execute the local binary.",
|
|
69
|
+
location: "target.command",
|
|
70
|
+
cwe: "CWE-829",
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
for (const candidate of SUSPICIOUS_ARGUMENTS) {
|
|
74
|
+
if (candidate.pattern.test(joinedArguments)) {
|
|
75
|
+
findings.push({
|
|
76
|
+
id: "LAUNCH003",
|
|
77
|
+
severity: "high",
|
|
78
|
+
title: "Suspicious launcher argument",
|
|
79
|
+
evidence: `Arguments contain a pattern associated with ${candidate.label}.`,
|
|
80
|
+
recommendation: "Replace the launcher with a fixed, reviewable command.",
|
|
81
|
+
location: "target.args",
|
|
82
|
+
cwe: "CWE-78",
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
for (const rule of INLINE_EVAL_RULES) {
|
|
87
|
+
if (rule.executables.has(executable) && target.args.some((arg) => rule.flags.test(arg))) {
|
|
88
|
+
findings.push({
|
|
89
|
+
id: "LAUNCH004",
|
|
90
|
+
severity: "high",
|
|
91
|
+
title: "Target executes inline code from the command line",
|
|
92
|
+
evidence: `"${executable}" is invoked with an inline evaluation flag instead of a reviewable script file.`,
|
|
93
|
+
recommendation: "Move the code into a committed, reviewable script and launch that file directly.",
|
|
94
|
+
location: "target.args",
|
|
95
|
+
cwe: "CWE-95",
|
|
96
|
+
});
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return findings;
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=launch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"launch.js","sourceRoot":"","sources":["../../../src/rules/launch.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Finding } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Decide whether it is safe for the scanner to fetch a URL taken from an
|
|
4
|
+
* untrusted server response.
|
|
5
|
+
* - "ok": public https (or loopback http when the operator's own target is local).
|
|
6
|
+
* - "ssrf": the host resolves to a private/loopback/link-local address the
|
|
7
|
+
* server should not be able to steer the scanner toward.
|
|
8
|
+
* - "unsafe": unparseable, non-http(s), unresolvable, or plaintext to a
|
|
9
|
+
* non-loopback host.
|
|
10
|
+
* `allowLoopback` is true only when the operator's target is itself local, so a
|
|
11
|
+
* local dev scan may follow local metadata.
|
|
12
|
+
*/
|
|
13
|
+
export declare function assessServerUrl(rawUrl: string, allowLoopback: boolean): Promise<"ok" | "ssrf" | "unsafe">;
|
|
14
|
+
/**
|
|
15
|
+
* Audit the OAuth 2.1 authorization posture of a remote MCP endpoint, following
|
|
16
|
+
* the MCP authorization spec (RFC 9728 Protected Resource Metadata + RFC 8414
|
|
17
|
+
* Authorization Server Metadata). Only meaningful for a server that challenges
|
|
18
|
+
* unauthenticated requests; an open server is covered by REMOTE004. Every URL
|
|
19
|
+
* taken from a server response is SSRF-checked before it is fetched.
|
|
20
|
+
*/
|
|
21
|
+
export declare function inspectRemoteAuth(rawUrl: string, timeoutMs: number): Promise<Finding[]>;
|