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.
@@ -0,0 +1,297 @@
1
+ import { lookup } from "node:dns/promises";
2
+ import { isIP } from "node:net";
3
+ const LOCAL_HOSTS = new Set(["localhost", "127.0.0.1", "::1", "0.0.0.0"]);
4
+ // The scanner follows server-controlled OAuth metadata URLs. Bound how many
5
+ // authorization servers it will fetch so a hostile server cannot use a long
6
+ // authorization_servers list to amplify requests (SSRF/DoS) from the scanner.
7
+ const MAX_AUTH_SERVERS = 5;
8
+ function isLocal(hostname) {
9
+ const host = hostname.replace(/^\[|\]$/g, "").toLowerCase();
10
+ return LOCAL_HOSTS.has(host) || host.endsWith(".localhost");
11
+ }
12
+ /** True for a plaintext http:// URL whose host is not local. */
13
+ function isPlaintextNonLocal(rawUrl) {
14
+ try {
15
+ const url = new URL(rawUrl);
16
+ return url.protocol === "http:" && !isLocal(url.hostname);
17
+ }
18
+ catch {
19
+ return false;
20
+ }
21
+ }
22
+ function ipv4Of(ip) {
23
+ const mapped = /^::ffff:(\d+\.\d+\.\d+\.\d+)$/i.exec(ip);
24
+ return mapped && mapped[1] !== undefined ? mapped[1] : ip;
25
+ }
26
+ function isLoopback(ip) {
27
+ const v4 = ipv4Of(ip);
28
+ return /^127\./.test(v4) || ip.toLowerCase() === "::1";
29
+ }
30
+ /**
31
+ * True for any address the scanner must not be steered toward by an untrusted
32
+ * server: RFC 1918 private ranges, loopback, link-local (incl. the cloud
33
+ * metadata address 169.254.169.254), unique-local IPv6, and unspecified.
34
+ */
35
+ function isPrivateOrReserved(ip) {
36
+ const v4 = ipv4Of(ip);
37
+ if (/^(0|10|127)\./.test(v4))
38
+ return true;
39
+ if (/^169\.254\./.test(v4))
40
+ return true;
41
+ if (/^192\.168\./.test(v4))
42
+ return true;
43
+ if (/^172\.(1[6-9]|2\d|3[01])\./.test(v4))
44
+ return true;
45
+ if (v4 === "0.0.0.0" || v4 === "255.255.255.255")
46
+ return true;
47
+ const lower = ip.toLowerCase();
48
+ if (lower === "::" || lower === "::1")
49
+ return true;
50
+ if (/^f[cd]/.test(lower))
51
+ return true; // fc00::/7 unique local
52
+ if (/^fe[89ab]/.test(lower))
53
+ return true; // fe80::/10 link local
54
+ return false;
55
+ }
56
+ async function resolveHost(host) {
57
+ if (isIP(host) !== 0) {
58
+ return [host];
59
+ }
60
+ try {
61
+ const records = await lookup(host, { all: true });
62
+ return records.map((record) => record.address);
63
+ }
64
+ catch {
65
+ return [];
66
+ }
67
+ }
68
+ /**
69
+ * Decide whether it is safe for the scanner to fetch a URL taken from an
70
+ * untrusted server response.
71
+ * - "ok": public https (or loopback http when the operator's own target is local).
72
+ * - "ssrf": the host resolves to a private/loopback/link-local address the
73
+ * server should not be able to steer the scanner toward.
74
+ * - "unsafe": unparseable, non-http(s), unresolvable, or plaintext to a
75
+ * non-loopback host.
76
+ * `allowLoopback` is true only when the operator's target is itself local, so a
77
+ * local dev scan may follow local metadata.
78
+ */
79
+ export async function assessServerUrl(rawUrl, allowLoopback) {
80
+ let url;
81
+ try {
82
+ url = new URL(rawUrl);
83
+ }
84
+ catch {
85
+ return "unsafe";
86
+ }
87
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
88
+ return "unsafe";
89
+ }
90
+ const host = url.hostname.replace(/^\[|\]$/g, "");
91
+ const addresses = await resolveHost(host);
92
+ if (addresses.length === 0) {
93
+ return "unsafe";
94
+ }
95
+ let allLoopback = true;
96
+ for (const address of addresses) {
97
+ if (isLoopback(address)) {
98
+ if (!allowLoopback) {
99
+ return "ssrf";
100
+ }
101
+ continue;
102
+ }
103
+ allLoopback = false;
104
+ if (isPrivateOrReserved(address)) {
105
+ return "ssrf";
106
+ }
107
+ }
108
+ // Plaintext is only tolerated for a loopback dev target; anything else must be https.
109
+ if (url.protocol === "http:" && !(allLoopback && allowLoopback)) {
110
+ return "unsafe";
111
+ }
112
+ return "ok";
113
+ }
114
+ async function safeFetch(url, timeoutMs, init) {
115
+ const controller = new AbortController();
116
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
117
+ timer.unref();
118
+ try {
119
+ // redirect: "manual" so a server cannot 3xx-hop past the SSRF pre-check.
120
+ const response = await fetch(url, { ...init, signal: controller.signal, redirect: "manual" });
121
+ let json;
122
+ try {
123
+ const parsed = await response.json();
124
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
125
+ json = parsed;
126
+ }
127
+ }
128
+ catch {
129
+ json = undefined;
130
+ }
131
+ return { status: response.status, headers: response.headers, json };
132
+ }
133
+ catch {
134
+ return undefined;
135
+ }
136
+ finally {
137
+ clearTimeout(timer);
138
+ }
139
+ }
140
+ function stringArray(value) {
141
+ return Array.isArray(value)
142
+ ? value.filter((entry) => typeof entry === "string")
143
+ : [];
144
+ }
145
+ /**
146
+ * Audit the OAuth 2.1 authorization posture of a remote MCP endpoint, following
147
+ * the MCP authorization spec (RFC 9728 Protected Resource Metadata + RFC 8414
148
+ * Authorization Server Metadata). Only meaningful for a server that challenges
149
+ * unauthenticated requests; an open server is covered by REMOTE004. Every URL
150
+ * taken from a server response is SSRF-checked before it is fetched.
151
+ */
152
+ export async function inspectRemoteAuth(rawUrl, timeoutMs) {
153
+ const findings = [];
154
+ let base;
155
+ try {
156
+ base = new URL(rawUrl);
157
+ }
158
+ catch {
159
+ return findings;
160
+ }
161
+ const targetIsLocal = isLocal(base.hostname);
162
+ const probe = await safeFetch(rawUrl, timeoutMs, {
163
+ method: "POST",
164
+ headers: {
165
+ "content-type": "application/json",
166
+ accept: "application/json, text/event-stream",
167
+ },
168
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "initialize", params: {} }),
169
+ });
170
+ // No response, or the server did not challenge: openness is REMOTE004's job.
171
+ if (probe === undefined || (probe.status !== 401 && probe.status !== 403)) {
172
+ return findings;
173
+ }
174
+ // RFC 9728: a protected resource must point to its metadata via WWW-Authenticate.
175
+ const wwwAuthenticate = probe.headers.get("www-authenticate") ?? "";
176
+ const pointer = /resource_metadata="?([^",\s]+)"?/i.exec(wwwAuthenticate);
177
+ let prmUrl;
178
+ if (pointer && pointer[1] !== undefined) {
179
+ prmUrl = pointer[1];
180
+ }
181
+ else {
182
+ findings.push({
183
+ id: "AUTH001",
184
+ severity: "medium",
185
+ title: "Protected server does not advertise its resource metadata",
186
+ evidence: `The server returned ${probe.status} without a WWW-Authenticate "resource_metadata" pointer (RFC 9728).`,
187
+ recommendation: 'Return WWW-Authenticate: Bearer resource_metadata="<url>" so clients can discover how to authenticate.',
188
+ location: "server",
189
+ cwe: "CWE-306",
190
+ owasp: "LLM08",
191
+ });
192
+ prmUrl = new URL("/.well-known/oauth-protected-resource", base).toString();
193
+ }
194
+ const prmSafety = await assessServerUrl(prmUrl, targetIsLocal);
195
+ if (prmSafety === "ssrf") {
196
+ findings.push(ssrfFinding("resource metadata", prmUrl));
197
+ return findings;
198
+ }
199
+ const prm = prmSafety === "ok" ? await safeFetch(prmUrl, timeoutMs) : undefined;
200
+ if (prm === undefined || prm.status >= 400 || prm.json === undefined) {
201
+ findings.push({
202
+ id: "AUTH002",
203
+ severity: "medium",
204
+ title: "OAuth Protected Resource Metadata is unreachable or malformed",
205
+ evidence: `Could not retrieve valid Protected Resource Metadata from ${prmUrl}.`,
206
+ recommendation: "Serve a valid RFC 9728 metadata document listing the authorization server(s).",
207
+ location: "server",
208
+ cwe: "CWE-306",
209
+ owasp: "LLM08",
210
+ });
211
+ return findings;
212
+ }
213
+ const authorizationServers = stringArray(prm.json.authorization_servers);
214
+ if (authorizationServers.length === 0) {
215
+ findings.push({
216
+ id: "AUTH002",
217
+ severity: "medium",
218
+ title: "Protected Resource Metadata lists no authorization server",
219
+ evidence: "The resource metadata does not contain a non-empty authorization_servers array.",
220
+ recommendation: "List at least one authorization server in the resource metadata.",
221
+ location: "server",
222
+ cwe: "CWE-306",
223
+ owasp: "LLM08",
224
+ });
225
+ return findings;
226
+ }
227
+ for (const authServer of authorizationServers.slice(0, MAX_AUTH_SERVERS)) {
228
+ if (isPlaintextNonLocal(authServer)) {
229
+ findings.push(plaintextEndpointFinding("authorization server", authServer));
230
+ }
231
+ const asMetaUrl = new URL("/.well-known/oauth-authorization-server", authServer).toString();
232
+ const asSafety = await assessServerUrl(asMetaUrl, targetIsLocal);
233
+ if (asSafety === "ssrf") {
234
+ findings.push(ssrfFinding("authorization server metadata", asMetaUrl));
235
+ continue;
236
+ }
237
+ const asMeta = asSafety === "ok" ? await safeFetch(asMetaUrl, timeoutMs) : undefined;
238
+ if (asMeta === undefined || asMeta.status >= 400 || asMeta.json === undefined) {
239
+ findings.push({
240
+ id: "AUTH002",
241
+ severity: "medium",
242
+ title: "Authorization Server Metadata is unreachable or malformed",
243
+ evidence: `Could not retrieve valid Authorization Server Metadata from ${asMetaUrl}.`,
244
+ recommendation: "Serve a valid RFC 8414 authorization server metadata document.",
245
+ location: "server",
246
+ cwe: "CWE-306",
247
+ owasp: "LLM08",
248
+ });
249
+ continue;
250
+ }
251
+ const pkceMethods = stringArray(asMeta.json.code_challenge_methods_supported);
252
+ if (!pkceMethods.includes("S256")) {
253
+ findings.push({
254
+ id: "AUTH003",
255
+ severity: "high",
256
+ title: "Authorization server does not advertise PKCE (S256)",
257
+ evidence: 'code_challenge_methods_supported does not include "S256".',
258
+ recommendation: "Require PKCE with S256; it is mandatory under OAuth 2.1 and the MCP authorization spec.",
259
+ location: "server",
260
+ cwe: "CWE-1188",
261
+ owasp: "LLM08",
262
+ });
263
+ }
264
+ for (const key of ["authorization_endpoint", "token_endpoint", "registration_endpoint"]) {
265
+ const endpoint = asMeta.json[key];
266
+ if (typeof endpoint === "string" && isPlaintextNonLocal(endpoint)) {
267
+ findings.push(plaintextEndpointFinding(key, endpoint));
268
+ }
269
+ }
270
+ }
271
+ return findings;
272
+ }
273
+ function ssrfFinding(label, url) {
274
+ return {
275
+ id: "AUTH005",
276
+ severity: "high",
277
+ title: "Server directed the scanner to a non-public address (possible SSRF)",
278
+ evidence: `The ${label} URL resolves to a loopback, link-local, or private address and was not fetched: ${url}`,
279
+ recommendation: "A remote server's OAuth metadata must reference publicly resolvable https endpoints, not internal addresses.",
280
+ location: "server",
281
+ cwe: "CWE-918",
282
+ owasp: "LLM08",
283
+ };
284
+ }
285
+ function plaintextEndpointFinding(label, endpoint) {
286
+ return {
287
+ id: "AUTH004",
288
+ severity: "high",
289
+ title: "OAuth endpoint uses plaintext HTTP",
290
+ evidence: `The ${label} is served over plaintext HTTP: ${endpoint}`,
291
+ recommendation: "Serve all OAuth endpoints over https://.",
292
+ location: "server",
293
+ cwe: "CWE-319",
294
+ owasp: "LLM08",
295
+ };
296
+ }
297
+ //# sourceMappingURL=oauth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth.js","sourceRoot":"","sources":["../../../src/rules/oauth.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import type { Finding, TargetConfig } from "../types.js";
2
+ /**
3
+ * Static checks on a remote target URL, evaluated without connecting — the
4
+ * remote counterpart of the launch-configuration rules for stdio targets.
5
+ */
6
+ export declare function inspectRemote(target: TargetConfig): Finding[];
7
+ /**
8
+ * Finding for a remote server that accepted our connection without any
9
+ * authentication. Evaluated only after a successful `--execute` connect, since
10
+ * the scanner never supplies credentials.
11
+ */
12
+ export declare function unauthenticatedAccessFinding(): Finding;
@@ -0,0 +1,81 @@
1
+ import { isCredentialQueryKey } from "../environment.js";
2
+ const LOCAL_HOSTS = new Set(["localhost", "127.0.0.1", "::1", "0.0.0.0"]);
3
+ function isLocal(hostname) {
4
+ const host = hostname.replace(/^\[|\]$/g, "").toLowerCase();
5
+ return LOCAL_HOSTS.has(host) || host.endsWith(".localhost");
6
+ }
7
+ /**
8
+ * Static checks on a remote target URL, evaluated without connecting — the
9
+ * remote counterpart of the launch-configuration rules for stdio targets.
10
+ */
11
+ export function inspectRemote(target) {
12
+ if (target.url === undefined) {
13
+ return [];
14
+ }
15
+ const findings = [];
16
+ let url;
17
+ try {
18
+ url = new URL(target.url);
19
+ }
20
+ catch {
21
+ findings.push({
22
+ id: "REMOTE001",
23
+ severity: "medium",
24
+ title: "Remote target URL is malformed",
25
+ evidence: "target.url could not be parsed as a URL.",
26
+ recommendation: "Provide a valid absolute URL for the MCP endpoint.",
27
+ location: "target.url",
28
+ cwe: "CWE-20",
29
+ });
30
+ return findings;
31
+ }
32
+ if (url.protocol === "http:" && !isLocal(url.hostname)) {
33
+ findings.push({
34
+ id: "REMOTE002",
35
+ severity: "high",
36
+ title: "Remote endpoint uses plaintext HTTP",
37
+ evidence: `The endpoint "${url.host}" is served over http://, so traffic and any tokens travel unencrypted.`,
38
+ recommendation: "Use https:// for any non-local MCP endpoint.",
39
+ location: "target.url",
40
+ cwe: "CWE-319",
41
+ owasp: "LLM08",
42
+ });
43
+ }
44
+ const hasUserInfo = url.username !== "" || url.password !== "";
45
+ const fragmentParams = new URLSearchParams(url.hash.replace(/^#/, ""));
46
+ const hasSensitiveQuery = [...url.searchParams.keys()].some(isCredentialQueryKey) ||
47
+ [...fragmentParams.keys()].some(isCredentialQueryKey);
48
+ if (hasUserInfo || hasSensitiveQuery) {
49
+ findings.push({
50
+ id: "REMOTE003",
51
+ severity: "high",
52
+ title: "Credentials are embedded in the target URL",
53
+ evidence: hasUserInfo
54
+ ? "The URL contains userinfo (user:password@), which leaks into logs and history."
55
+ : "The URL carries a credential in a query parameter, which leaks into logs and history.",
56
+ recommendation: "Pass credentials through an authentication header or OAuth flow, never in the URL.",
57
+ location: "target.url",
58
+ cwe: "CWE-522",
59
+ owasp: "LLM08",
60
+ });
61
+ }
62
+ return findings;
63
+ }
64
+ /**
65
+ * Finding for a remote server that accepted our connection without any
66
+ * authentication. Evaluated only after a successful `--execute` connect, since
67
+ * the scanner never supplies credentials.
68
+ */
69
+ export function unauthenticatedAccessFinding() {
70
+ return {
71
+ id: "REMOTE004",
72
+ severity: "medium",
73
+ title: "Remote MCP server accepts unauthenticated connections",
74
+ evidence: "The server completed the MCP handshake and listed tools without any credentials.",
75
+ recommendation: "Require authentication (e.g. OAuth) for remote MCP servers unless anonymous access is intended.",
76
+ location: "server",
77
+ cwe: "CWE-306",
78
+ owasp: "LLM08",
79
+ };
80
+ }
81
+ //# sourceMappingURL=remote.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remote.js","sourceRoot":"","sources":["../../../src/rules/remote.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ import type { Finding, ToolMetadata } from "../types.js";
2
+ export declare function inspectServerCapabilities(tools: ToolMetadata[]): Finding[];
3
+ /**
4
+ * Inspect the server-level instructions returned during the MCP handshake.
5
+ * These are injected verbatim into the model's context, so injection here is
6
+ * as dangerous as in any tool description.
7
+ */
8
+ export declare function inspectServerInstructions(instructions: string | undefined): Finding[];
9
+ export declare function inspectTool(tool: ToolMetadata): Finding[];
@@ -0,0 +1,216 @@
1
+ import { scanTextForInjection } from "./injection.js";
2
+ const DESTRUCTIVE_NAME_PATTERN = /(?:^|[_-])(delete|drop|destroy|remove|reset|purge|revoke|send|publish|deploy)(?:$|[_-])/i;
3
+ const READ_NAME_PATTERN = /(?:^|[_-])(get|list|read|search|find|fetch|inspect|describe)(?:$|[_-])/i;
4
+ function propertiesOf(schema) {
5
+ const properties = schema.properties;
6
+ if (typeof properties !== "object" || properties === null || Array.isArray(properties)) {
7
+ return {};
8
+ }
9
+ return properties;
10
+ }
11
+ function annotationBoolean(tool, key) {
12
+ const value = tool.annotations?.[key];
13
+ return typeof value === "boolean" ? value : undefined;
14
+ }
15
+ /**
16
+ * Collect every untrusted string a server places into the model's context for a
17
+ * tool: the description, the annotation title, and each parameter's name,
18
+ * description, and string-literal enum values. Injection can hide in any of
19
+ * these, not only in the top-level description.
20
+ */
21
+ function injectionSurface(tool) {
22
+ const surface = [];
23
+ if (typeof tool.description === "string") {
24
+ surface.push({ text: tool.description, sourceLabel: "description" });
25
+ }
26
+ const title = tool.annotations?.title;
27
+ if (typeof title === "string") {
28
+ surface.push({ text: title, sourceLabel: "annotation title" });
29
+ }
30
+ for (const [name, schema] of Object.entries(propertiesOf(tool.inputSchema))) {
31
+ surface.push({ text: name, sourceLabel: `parameter name "${name}"` });
32
+ if (typeof schema === "object" && schema !== null && !Array.isArray(schema)) {
33
+ const record = schema;
34
+ if (typeof record.description === "string") {
35
+ surface.push({ text: record.description, sourceLabel: `parameter "${name}" description` });
36
+ }
37
+ if (Array.isArray(record.enum)) {
38
+ for (const value of record.enum) {
39
+ if (typeof value === "string") {
40
+ surface.push({ text: value, sourceLabel: `parameter "${name}" enum value` });
41
+ }
42
+ }
43
+ }
44
+ }
45
+ }
46
+ return surface;
47
+ }
48
+ export function inspectServerCapabilities(tools) {
49
+ const findings = [];
50
+ let hasRead = false;
51
+ let hasWrite = false;
52
+ for (const tool of tools) {
53
+ if (READ_NAME_PATTERN.test(tool.name)) {
54
+ hasRead = true;
55
+ }
56
+ if (DESTRUCTIVE_NAME_PATTERN.test(tool.name) ||
57
+ tool.name.match(/^(?:write|update|set|put|post|create|insert)/i)) {
58
+ hasWrite = true;
59
+ }
60
+ }
61
+ if (hasRead && hasWrite) {
62
+ findings.push({
63
+ id: "TOOL011",
64
+ severity: "high",
65
+ title: "Dangerous Capability Combination (Least Privilege Violation)",
66
+ evidence: "The server exposes both broad READ tools and destructive WRITE tools.",
67
+ recommendation: "Separate read and write capabilities into different MCP servers or require explicit user confirmation for write operations.",
68
+ location: "server",
69
+ cwe: "CWE-250",
70
+ owasp: "LLM08",
71
+ });
72
+ }
73
+ return findings;
74
+ }
75
+ /**
76
+ * Inspect the server-level instructions returned during the MCP handshake.
77
+ * These are injected verbatim into the model's context, so injection here is
78
+ * as dangerous as in any tool description.
79
+ */
80
+ export function inspectServerInstructions(instructions) {
81
+ if (typeof instructions !== "string") {
82
+ return [];
83
+ }
84
+ return scanTextForInjection(instructions, "server", "server instructions").map((finding) => ({
85
+ ...finding,
86
+ title: finding.id === "TOOL003"
87
+ ? "Server instructions contain a prompt-injection-like instruction"
88
+ : finding.title,
89
+ }));
90
+ }
91
+ export function inspectTool(tool) {
92
+ const findings = [];
93
+ const location = `tool:${tool.name}`;
94
+ const annotations = tool.annotations ?? {};
95
+ const description = tool.description ?? "";
96
+ if (tool.name.length > 64) {
97
+ findings.push({
98
+ id: "TOOL001",
99
+ severity: "high",
100
+ title: "Tool name exceeds the compatibility limit",
101
+ evidence: `Tool name has ${tool.name.length} characters.`,
102
+ recommendation: "Use a stable tool name with at most 64 characters.",
103
+ location,
104
+ cwe: "CWE-20",
105
+ });
106
+ }
107
+ if (description.trim() === "") {
108
+ findings.push({
109
+ id: "TOOL002",
110
+ severity: "medium",
111
+ title: "Tool description is missing",
112
+ evidence: "The server returned no description for this tool.",
113
+ recommendation: "Describe the tool's precise behavior and when it is appropriate.",
114
+ location,
115
+ });
116
+ }
117
+ for (const { text, sourceLabel } of injectionSurface(tool)) {
118
+ findings.push(...scanTextForInjection(text, location, sourceLabel));
119
+ }
120
+ if (typeof annotations.title !== "string" || annotations.title.trim() === "") {
121
+ findings.push({
122
+ id: "TOOL004",
123
+ severity: "medium",
124
+ title: "Tool annotation title is missing",
125
+ evidence: "annotations.title is absent or empty.",
126
+ recommendation: "Add a short human-readable title annotation.",
127
+ location,
128
+ });
129
+ }
130
+ const destructiveHint = annotationBoolean(tool, "destructiveHint");
131
+ const readOnlyHint = annotationBoolean(tool, "readOnlyHint");
132
+ if (destructiveHint === undefined && readOnlyHint === undefined) {
133
+ findings.push({
134
+ id: "TOOL005",
135
+ severity: "high",
136
+ title: "Tool safety hints are missing",
137
+ evidence: "Neither destructiveHint nor readOnlyHint is explicitly declared.",
138
+ recommendation: "Declare the applicable MCP safety annotations explicitly.",
139
+ location,
140
+ cwe: "CWE-1188",
141
+ });
142
+ }
143
+ if (DESTRUCTIVE_NAME_PATTERN.test(tool.name) && destructiveHint !== true) {
144
+ findings.push({
145
+ id: "TOOL006",
146
+ severity: "high",
147
+ title: "Potentially destructive tool is not marked destructive",
148
+ evidence: `Tool name "${tool.name}" implies an external side effect.`,
149
+ recommendation: "Set destructiveHint to true and keep the operation narrowly scoped.",
150
+ location,
151
+ cwe: "CWE-250",
152
+ owasp: "LLM08",
153
+ });
154
+ }
155
+ if (READ_NAME_PATTERN.test(tool.name) && readOnlyHint === false) {
156
+ findings.push({
157
+ id: "TOOL007",
158
+ severity: "medium",
159
+ title: "Read-like tool is explicitly marked as writable",
160
+ evidence: `Tool name "${tool.name}" appears read-only but readOnlyHint is false.`,
161
+ recommendation: "Split reading and mutation into separate tools with accurate hints.",
162
+ location,
163
+ });
164
+ }
165
+ const properties = propertiesOf(tool.inputSchema);
166
+ const methodSchema = properties.method;
167
+ if (typeof methodSchema === "object" && methodSchema !== null && !Array.isArray(methodSchema)) {
168
+ const methodEnum = methodSchema.enum;
169
+ if (Array.isArray(methodEnum)) {
170
+ const methods = new Set(methodEnum
171
+ .filter((value) => typeof value === "string")
172
+ .map((value) => value.toUpperCase()));
173
+ const hasSafe = ["GET", "HEAD", "OPTIONS"].some((method) => methods.has(method));
174
+ const hasUnsafe = ["POST", "PUT", "PATCH", "DELETE"].some((method) => methods.has(method));
175
+ if (hasSafe && hasUnsafe) {
176
+ findings.push({
177
+ id: "TOOL008",
178
+ severity: "high",
179
+ title: "Tool mixes read and write HTTP operations",
180
+ evidence: `method enum contains both safe and unsafe HTTP methods: ${[...methods].join(", ")}.`,
181
+ recommendation: "Split read operations from create, update, and delete tools.",
182
+ location,
183
+ cwe: "CWE-250",
184
+ owasp: "LLM08",
185
+ });
186
+ }
187
+ }
188
+ }
189
+ if (tool.inputSchema.additionalProperties !== false) {
190
+ findings.push({
191
+ id: "TOOL009",
192
+ severity: "low",
193
+ title: "Input schema accepts undeclared properties",
194
+ evidence: "inputSchema.additionalProperties is not explicitly false.",
195
+ recommendation: "Reject unknown input fields when the server framework supports it.",
196
+ location,
197
+ cwe: "CWE-20",
198
+ });
199
+ }
200
+ const paginationKeys = new Set(["limit", "offset", "cursor", "max_results", "page"]);
201
+ const hasPagination = Object.keys(properties).some((key) => paginationKeys.has(key.toLowerCase()));
202
+ if (!hasPagination && READ_NAME_PATTERN.test(tool.name)) {
203
+ findings.push({
204
+ id: "TOOL010",
205
+ severity: "medium",
206
+ title: "Context Exhaustion Risk: Missing pagination limits",
207
+ evidence: "Tool name implies reading, but inputSchema lacks limit, cursor, or offset fields.",
208
+ recommendation: "Implement pagination limits to prevent context window exhaustion attacks.",
209
+ location,
210
+ cwe: "CWE-400",
211
+ owasp: "LLM10",
212
+ });
213
+ }
214
+ return findings;
215
+ }
216
+ //# sourceMappingURL=tools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tools.js","sourceRoot":"","sources":["../../../src/rules/tools.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import type { ScanConfig, ScanReport } from "./types.js";
2
+ export declare function scan(config: ScanConfig, execute: boolean, sandbox?: "docker" | "none", fuzz?: boolean): Promise<ScanReport>;