Mostly safe โ a couple of notes worth reading.
Scanned 5/3/2026, 6:33:23 PMยทCached resultยทFast Scanยท45 rulesยทHow we decide โ
AIVSS Score
Low
Severity Breakdown
0
critical
2
high
39
medium
0
low
MCP Server Information
Findings
This package has a B grade with a safety score of 66/100 and carries moderate risk due to 39 medium-severity issues, primarily involving ansi escape injection vulnerabilities (16 instances), server configuration weaknesses (17 instances), and resource exhaustion risks (7 instances). The two high-severity findings warrant careful review before installation, though the absence of critical issues suggests the risks are manageable with proper deployment controls. Consider implementing input validation and secure server configuration practices if you proceed with this package.
No known CVEs found for this package or its dependencies.
Scan Details
Want deeper analysis?
Fast scan found 41 findings using rule-based analysis. Upgrade for LLM consensus across 5 judges, AI-generated remediation, and cross-file taint analysis.
Building your own MCP server?
Same rules, same LLM judges, same grade. Private scans stay isolated to your account and never appear in the public registry. Required for code your team hasnโt shipped yet.
Showing 1โ30 of 41 findings
41 findings
File mounts an HTTP route that handles MCP `tools/list` (Express / Fastify / FastAPI / Flask) but the route โ and the router it sits behind โ has no auth middleware applied. An anonymous client can enumerate every tool the server exposes, scope the attack surface, and (if `tools/call` shares the route) invoke them. Apply auth at the route or router level: Express `passport.authenticate(...)` / a `requireAuth`-style middleware, FastAPI `Depends(get_current_user)` or `Depends(verify_jwt)`, Flask
Evidence
| 1 | import express, { Request as ExpressRequest, Response, NextFunction } from 'express'; |
| 2 | import { randomUUID } from "node:crypto"; |
| 3 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; |
| 4 | import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; |
| 5 | import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; |
| 6 | import { z } from 'zod'; |
| 7 | import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js"; |
| 8 | import { DataForSEOClient, |
Remediation
Apply auth middleware at the route or router level: - Express / Fastify / Koa: `passport.authenticate(...)`, `requireAuth`, `verifyToken`, or an equivalent JWT middleware applied via `router.use(authMw)` or as a per-route handler. - FastAPI: `Depends(get_current_user)`, `OAuth2PasswordBearer`, `HTTPBearer`, or `verify_jwt` dependency. - Flask: `@login_required`, `@auth_required`, `@jwt_required`, or call `verify_jwt_in_request()` in the handler. Mounting MCP behind a s
File mounts an HTTP route that handles MCP `tools/list` (Express / Fastify / FastAPI / Flask) but the route โ and the router it sits behind โ has no auth middleware applied. An anonymous client can enumerate every tool the server exposes, scope the attack surface, and (if `tools/call` shares the route) invoke them. Apply auth at the route or router level: Express `passport.authenticate(...)` / a `requireAuth`-style middleware, FastAPI `Depends(get_current_user)` or `Depends(verify_jwt)`, Flask
Evidence
| 1 | #!/usr/bin/env node |
| 2 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; |
| 3 | import { DataForSEOClient, DataForSEOConfig } from '../core/client/dataforseo.client.js'; |
| 4 | import { SerpApiModule } from '../core/modules/serp/serp-api.module.js'; |
| 5 | import { KeywordsDataApiModule } from '../core/modules/keywords-data/keywords-data-api.module.js'; |
| 6 | import { OnPageApiModule } from '../core/modules/onpage/onpage-api.module.js'; |
| 7 | import { DataForSEOLabsApi } from '../core/modules/dataforseo-labs/dat |
Remediation
Apply auth middleware at the route or router level: - Express / Fastify / Koa: `passport.authenticate(...)`, `requireAuth`, `verifyToken`, or an equivalent JWT middleware applied via `router.use(authMw)` or as a per-route handler. - FastAPI: `Depends(get_current_user)`, `OAuth2PasswordBearer`, `HTTPBearer`, or `verify_jwt` dependency. - Flask: `@login_required`, `@auth_required`, `@jwt_required`, or call `verify_jwt_in_request()` in the handler. Mounting MCP behind a s
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 45 | async handle(params:any): Promise<any> { |
| 46 | try { |
| 47 | console.error(JSON.stringify(params, null, 2)); |
| 48 | const response = await this.dataForSEOClient.makeRequest(`/v3/serp/youtube/organic/live/advanced`, 'POST', [{ |
| 49 | keyword: params.keyword, |
| 50 | location_name: params.location_name, |
Remediation
Strip C0/C1 control codes before printing user-controlled values. Python: re.sub(r"[\x00-\x08\x0b-\x1f\x7f]", "", s). Prefer a structured logger (json/logfmt) over raw print to stdout.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 41 | async handle(params:any): Promise<any> { |
| 42 | try { |
| 43 | console.error(JSON.stringify(params, null, 2)); |
| 44 | const response = await this.dataForSEOClient.makeRequest(`/v3/serp/youtube/video_info/live/advanced`, 'POST', [{ |
| 45 | video_id: params.video_id, |
| 46 | location_name: params.location_name, |
Remediation
Strip C0/C1 control codes before printing user-controlled values. Python: re.sub(r"[\x00-\x08\x0b-\x1f\x7f]", "", s). Prefer a structured logger (json/logfmt) over raw print to stdout.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 107 | await server.connect(transport); |
| 108 | console.error('handle request'); |
| 109 | await transport.handleRequest(req , res, req.body); |
| 110 | console.error('end handle request'); |
| 111 | req.on('close', () => { |
| 112 | console.error('Request closed'); |
| 113 | transport.close(); |
Remediation
Strip C0/C1 control codes before printing user-controlled values. Python: re.sub(r"[\x00-\x08\x0b-\x1f\x7f]", "", s). Prefer a structured logger (json/logfmt) over raw print to stdout.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 47 | async handle(params:any): Promise<any> { |
| 48 | try { |
| 49 | console.error(JSON.stringify(params, null, 2)); |
| 50 | const response = await this.dataForSEOClient.makeRequest(`/v3/serp/${params.search_engine}/organic/live/advanced`, 'POST', [{ |
| 51 | location_name: params.location_name, |
| 52 | language_code: params.language_code, |
Remediation
Strip C0/C1 control codes before printing user-controlled values. Python: re.sub(r"[\x00-\x08\x0b-\x1f\x7f]", "", s). Prefer a structured logger (json/logfmt) over raw print to stdout.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 170 | }; |
| 171 | |
| 172 | const handleNotAllowed = (method: string) => async (req: Request, res: Response) => { |
| 173 | console.error(`Received ${method} request`); |
| 174 | res.status(405).json({ |
| 175 | jsonrpc: "2.0", |
| 176 | error: { |
Remediation
Strip C0/C1 control codes before printing user-controlled values. Python: re.sub(r"[\x00-\x08\x0b-\x1f\x7f]", "", s). Prefer a structured logger (json/logfmt) over raw print to stdout.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 155 | }); |
| 156 | |
| 157 | } catch (error) { |
| 158 | console.error('Error handling MCP request:', error); |
| 159 | if (!res.headersSent) { |
| 160 | res.status(500).json({ |
| 161 | jsonrpc: '2.0', |
Remediation
Strip C0/C1 control codes before printing user-controlled values. Python: re.sub(r"[\x00-\x08\x0b-\x1f\x7f]", "", s). Prefer a structured logger (json/logfmt) over raw print to stdout.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 105 | }); |
| 106 | |
| 107 | await server.connect(transport); |
| 108 | console.error('handle request'); |
| 109 | await transport.handleRequest(req , res, req.body); |
| 110 | console.error('end handle request'); |
| 111 | req.on('close', () => { |
Remediation
Strip C0/C1 control codes before printing user-controlled values. Python: re.sub(r"[\x00-\x08\x0b-\x1f\x7f]", "", s). Prefer a structured logger (json/logfmt) over raw print to stdout.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 130 | }; |
| 131 | |
| 132 | const handleNotAllowed = (method: string) => async (req: Request, res: Response) => { |
| 133 | console.error(`Received ${method} request`); |
| 134 | res.status(405).json({ |
| 135 | jsonrpc: "2.0", |
| 136 | error: { |
Remediation
Strip C0/C1 control codes before printing user-controlled values. Python: re.sub(r"[\x00-\x08\x0b-\x1f\x7f]", "", s). Prefer a structured logger (json/logfmt) over raw print to stdout.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 145 | }); |
| 146 | |
| 147 | await server.connect(transport); |
| 148 | console.error('handle request'); |
| 149 | await transport.handleRequest(req , res, req.body); |
| 150 | console.error('end handle request'); |
| 151 | req.on('close', () => { |
Remediation
Strip C0/C1 control codes before printing user-controlled values. Python: re.sub(r"[\x00-\x08\x0b-\x1f\x7f]", "", s). Prefer a structured logger (json/logfmt) over raw print to stdout.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 27 | 'User-Agent': `DataForSEO-MCP-TypeScript-SDK/${version}` |
| 28 | }; |
| 29 | |
| 30 | console.error(`Making request to ${url} with method ${method} and body`, body); |
| 31 | const response = await fetch(url, { |
| 32 | method, |
| 33 | headers, |
Remediation
Strip C0/C1 control codes before printing user-controlled values. Python: re.sub(r"[\x00-\x08\x0b-\x1f\x7f]", "", s). Prefer a structured logger (json/logfmt) over raw print to stdout.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 42 | async handle(params: any): Promise<any> { |
| 43 | try { |
| 44 | console.error(JSON.stringify(params, null, 2)); |
| 45 | const response = await this.dataForSEOClient.makeRequest(`/v3/serp/youtube/video_comments/live/advanced`, 'POST', [{ |
| 46 | video_id: params.video_id, |
| 47 | location_name: params.location_name, |
Remediation
Strip C0/C1 control codes before printing user-controlled values. Python: re.sub(r"[\x00-\x08\x0b-\x1f\x7f]", "", s). Prefer a structured logger (json/logfmt) over raw print to stdout.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 196 | //============================================================================= |
| 197 | |
| 198 | app.get('/sse', basicAuth, async (req: Request, res: Response) => { |
| 199 | console.log('Received GET request to /sse (deprecated SSE transport)'); |
| 200 | |
| 201 | // Handle credentials |
| 202 | if (!req.username && !req.password) { |
Remediation
Strip C0/C1 control codes before printing user-controlled values. Python: re.sub(r"[\x00-\x08\x0b-\x1f\x7f]", "", s). Prefer a structured logger (json/logfmt) over raw print to stdout.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 27 | async handle(params: any): Promise<any> { |
| 28 | try { |
| 29 | console.error(JSON.stringify(params, null, 2)); |
| 30 | const response = await this.dataForSEOClient.makeRequest(`/v3/ai_optimization/ai_keyword_data/keywords_search_volume/live`, 'POST', [{ |
| 31 | keywords: params.keywords, |
| 32 | location_name: params.location_name, |
Remediation
Strip C0/C1 control codes before printing user-controlled values. Python: re.sub(r"[\x00-\x08\x0b-\x1f\x7f]", "", s). Prefer a structured logger (json/logfmt) over raw print to stdout.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 115 | }); |
| 116 | |
| 117 | } catch (error) { |
| 118 | console.error('Error handling MCP request:', error); |
| 119 | if (!res.headersSent) { |
| 120 | res.status(500).json({ |
| 121 | jsonrpc: '2.0', |
Remediation
Strip C0/C1 control codes before printing user-controlled values. Python: re.sub(r"[\x00-\x08\x0b-\x1f\x7f]", "", s). Prefer a structured logger (json/logfmt) over raw print to stdout.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 147 | await server.connect(transport); |
| 148 | console.error('handle request'); |
| 149 | await transport.handleRequest(req , res, req.body); |
| 150 | console.error('end handle request'); |
| 151 | req.on('close', () => { |
| 152 | console.error('Request closed'); |
| 153 | transport.close(); |
Remediation
Strip C0/C1 control codes before printing user-controlled values. Python: re.sub(r"[\x00-\x08\x0b-\x1f\x7f]", "", s). Prefer a structured logger (json/logfmt) over raw print to stdout.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 43 | async handle(params:any): Promise<any> { |
| 44 | try { |
| 45 | console.error(JSON.stringify(params, null, 2)); |
| 46 | const response = await this.dataForSEOClient.makeRequest(`/v3/serp/youtube/video_subtitles/live/advanced`, 'POST', [{ |
| 47 | video_id: params.video_id, |
| 48 | location_name: params.location_name, |
Remediation
Strip C0/C1 control codes before printing user-controlled values. Python: re.sub(r"[\x00-\x08\x0b-\x1f\x7f]", "", s). Prefer a structured logger (json/logfmt) over raw print to stdout.
Network / IO / subprocess call without an explicit timeout. A malicious or hung upstream (HTTP host, socket peer, child process) can pin threads, exhaust connection/process pools, and make the MCP server unresponsive. Always pass a bounded timeout. v2 extends v1 with subprocess coverage (R03 from the legacy readiness audit).
Evidence
| 83 | } |
| 84 | |
| 85 | export default { |
| 86 | async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> { |
| 87 | const url = new URL(request.url); |
| 88 | |
| 89 | // Store environment in global context for McpAgent access |
Remediation
Pass timeout= on every call: - HTTP: `requests.get(url, timeout=5)`, `httpx.get(url, timeout=5.0)` - Node fetch: `AbortSignal.timeout(5000)` - Subprocess: `subprocess.run(["cmd"], timeout=30, check=True)` Pick a value short enough to fail fast and retry.
Network / IO / subprocess call without an explicit timeout. A malicious or hung upstream (HTTP host, socket peer, child process) can pin threads, exhaust connection/process pools, and make the MCP server unresponsive. Always pass a bounded timeout. v2 extends v1 with subprocess coverage (R03 from the legacy readiness audit).
Evidence
| 108 | } |
| 109 | // MCP endpoints using McpAgent pattern |
| 110 | if (url.pathname === "/sse" || url.pathname === "/sse/message") { |
| 111 | return DataForSEOMcpAgent.serveSSE("/sse").fetch(request, env, ctx); |
| 112 | } |
| 113 | |
| 114 | if (url.pathname === "/mcp" || url.pathname == '/http') { |
Remediation
Pass timeout= on every call: - HTTP: `requests.get(url, timeout=5)`, `httpx.get(url, timeout=5.0)` - Node fetch: `AbortSignal.timeout(5000)` - Subprocess: `subprocess.run(["cmd"], timeout=30, check=True)` Pick a value short enough to fail fast and retry.
Network / IO / subprocess call without an explicit timeout. A malicious or hung upstream (HTTP host, socket peer, child process) can pin threads, exhaust connection/process pools, and make the MCP server unresponsive. Always pass a bounded timeout. v2 extends v1 with subprocess coverage (R03 from the legacy readiness audit).
Evidence
| 318 | /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/reportError) */ |
| 319 | declare function reportError(error: any): void; |
| 320 | /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/fetch) */ |
| 321 | declare function fetch(input: RequestInfo | URL, init?: RequestInit<RequestInitCfProperties>): Promise<Response>; |
| 322 | declare const self: ServiceWorkerGlobalScope; |
| 323 | /** |
| 324 | * The Web Crypto API provides a set of low-level functions for common cryptographic tasks. |
Remediation
Pass timeout= on every call: - HTTP: `requests.get(url, timeout=5)`, `httpx.get(url, timeout=5.0)` - Node fetch: `AbortSignal.timeout(5000)` - Subprocess: `subprocess.run(["cmd"], timeout=30, check=True)` Pick a value short enough to fail fast and retry.
Network / IO / subprocess call without an explicit timeout. A malicious or hung upstream (HTTP host, socket peer, child process) can pin threads, exhaust connection/process pools, and make the MCP server unresponsive. Always pass a bounded timeout. v2 extends v1 with subprocess coverage (R03 from the legacy readiness audit).
Evidence
| 407 | readonly compatibilityFlags: Record<string, boolean>; |
| 408 | } |
| 409 | interface DurableObject { |
| 410 | fetch(request: Request): Response | Promise<Response>; |
| 411 | alarm?(alarmInfo?: AlarmInvocationInfo): void | Promise<void>; |
| 412 | webSocketMessage?(ws: WebSocket, message: string | ArrayBuffer): void | Promise<void>; |
| 413 | webSocketClose?(ws: WebSocket, code: number, reason: string, wasClean: boolean): void | Promise<void>; |
Remediation
Pass timeout= on every call: - HTTP: `requests.get(url, timeout=5)`, `httpx.get(url, timeout=5.0)` - Node fetch: `AbortSignal.timeout(5000)` - Subprocess: `subprocess.run(["cmd"], timeout=30, check=True)` Pick a value short enough to fail fast and retry.
Network / IO / subprocess call without an explicit timeout. A malicious or hung upstream (HTTP host, socket peer, child process) can pin threads, exhaust connection/process pools, and make the MCP server unresponsive. Always pass a bounded timeout. v2 extends v1 with subprocess coverage (R03 from the legacy readiness audit).
Evidence
| 1421 | } |
| 1422 | type Service<T extends Rpc.WorkerEntrypointBranded | undefined = undefined> = Fetcher<T>; |
| 1423 | type Fetcher<T extends Rpc.EntrypointBranded | undefined = undefined, Reserved extends string = never> = (T extends Rpc.EntrypointBranded ? Rpc.Provider<T, Reserved | "fetch" | "connect"> : unknown) & { |
| 1424 | fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>; |
| 1425 | connect(address: SocketAddress | string, options?: SocketOptions): Socket; |
| 1426 | }; |
| 1427 | interface KVNamespaceListKey<Metadata, Key exte |
Remediation
Pass timeout= on every call: - HTTP: `requests.get(url, timeout=5)`, `httpx.get(url, timeout=5.0)` - Node fetch: `AbortSignal.timeout(5000)` - Subprocess: `subprocess.run(["cmd"], timeout=30, check=True)` Pick a value short enough to fail fast and retry.
Network / IO / subprocess call without an explicit timeout. A malicious or hung upstream (HTTP host, socket peer, child process) can pin threads, exhaust connection/process pools, and make the MCP server unresponsive. Always pass a bounded timeout. v2 extends v1 with subprocess coverage (R03 from the legacy readiness audit).
Evidence
| 222 | queueMicrotask(task: Function): void; |
| 223 | structuredClone<T>(value: T, options?: StructuredSerializeOptions): T; |
| 224 | reportError(error: any): void; |
| 225 | fetch(input: RequestInfo | URL, init?: RequestInit<RequestInitCfProperties>): Promise<Response>; |
| 226 | self: ServiceWorkerGlobalScope; |
| 227 | crypto: Crypto; |
| 228 | caches: CacheStorage; |
Remediation
Pass timeout= on every call: - HTTP: `requests.get(url, timeout=5)`, `httpx.get(url, timeout=5.0)` - Node fetch: `AbortSignal.timeout(5000)` - Subprocess: `subprocess.run(["cmd"], timeout=30, check=True)` Pick a value short enough to fail fast and retry.
Network / IO / subprocess call without an explicit timeout. A malicious or hung upstream (HTTP host, socket peer, child process) can pin threads, exhaust connection/process pools, and make the MCP server unresponsive. Always pass a bounded timeout. v2 extends v1 with subprocess coverage (R03 from the legacy readiness audit).
Evidence
| 112 | } |
| 113 | |
| 114 | if (url.pathname === "/mcp" || url.pathname == '/http') { |
| 115 | return DataForSEOMcpAgent.serve("/mcp").fetch(request, env, ctx); |
| 116 | } |
| 117 | |
| 118 | return new Response("Not found", { status: 404 }); |
Remediation
Pass timeout= on every call: - HTTP: `requests.get(url, timeout=5)`, `httpx.get(url, timeout=5.0)` - Node fetch: `AbortSignal.timeout(5000)` - Subprocess: `subprocess.run(["cmd"], timeout=30, check=True)` Pick a value short enough to fail fast and retry.
MCP tool input schema exposes an unconstrained string/any field with a risky name (command/query/sql/code/script/url/path/expr/ eval). Any caller can pass arbitrary values, which typically widens the tool's blast radius well beyond its intent. Narrow the schema with `.enum()`, `.regex()`, `.max()`, `Literal[...]`, Pydantic `Field(max_length=..., pattern=...)`, or a JSON Schema `enum` / `pattern` / `maxLength`.
Evidence
| 7 | name: 'dfs-crawlability-audit', |
| 8 | title: 'Identify technical performance issues affecting crawlability and ranking', |
| 9 | params: { |
| 10 | url: z.string().describe('The URL of the page to analyze'), |
| 11 | }, |
| 12 | handler: async (params) => { |
| 13 | return { |
Remediation
Shape the schema to the tool's actual intent: - Zod: chain `.enum([...])`, `.regex(/.../)`, or `.max(n)`; prefer `z.enum([...])` or `z.literal(...)` when the value set is small. - Pydantic: use `Literal["a", "b"]` or `Field(max_length=..., pattern=r"...")`. - JSON Schema: add `"enum"`, `"pattern"`, or `"maxLength"` to the property. An overbroad schema is an "overpowered tool" โ the model has nothing to prevent it from calling the tool with input far beyond what the tool's prose contract
MCP tool input schema exposes an unconstrained string/any field with a risky name (command/query/sql/code/script/url/path/expr/ eval). Any caller can pass arbitrary values, which typically widens the tool's blast radius well beyond its intent. Narrow the schema with `.enum()`, `.regex()`, `.max()`, `Literal[...]`, Pydantic `Field(max_length=..., pattern=...)`, or a JSON Schema `enum` / `pattern` / `maxLength`.
Evidence
| 47 | name: 'dfs-performance-mobile-check', |
| 48 | title: 'Check for slow load time and mobile compatibility issues', |
| 49 | params: { |
| 50 | url: z.string().describe('The URL of the page to analyze'), |
| 51 | }, |
| 52 | handler: async (params) => { |
| 53 | return { |
Remediation
Shape the schema to the tool's actual intent: - Zod: chain `.enum([...])`, `.regex(/.../)`, or `.max(n)`; prefer `z.enum([...])` or `z.literal(...)` when the value set is small. - Pydantic: use `Literal["a", "b"]` or `Field(max_length=..., pattern=r"...")`. - JSON Schema: add `"enum"`, `"pattern"`, or `"maxLength"` to the property. An overbroad schema is an "overpowered tool" โ the model has nothing to prevent it from calling the tool with input far beyond what the tool's prose contract
MCP tool input schema exposes an unconstrained string/any field with a risky name (command/query/sql/code/script/url/path/expr/ eval). Any caller can pass arbitrary values, which typically widens the tool's blast radius well beyond its intent. Narrow the schema with `.enum()`, `.regex()`, `.max()`, `Literal[...]`, Pydantic `Field(max_length=..., pattern=...)`, or a JSON Schema `enum` / `pattern` / `maxLength`.
Evidence
| 27 | name: 'dfs-meta-tags-check', |
| 28 | title: 'Detect missing or duplicate meta tags hurting SEO', |
| 29 | params: { |
| 30 | url: z.string().describe('The URL of the page to analyze'), |
| 31 | }, |
| 32 | handler: async (params) => { |
| 33 | return { |
Remediation
Shape the schema to the tool's actual intent: - Zod: chain `.enum([...])`, `.regex(/.../)`, or `.max(n)`; prefer `z.enum([...])` or `z.literal(...)` when the value set is small. - Pydantic: use `Literal["a", "b"]` or `Field(max_length=..., pattern=r"...")`. - JSON Schema: add `"enum"`, `"pattern"`, or `"maxLength"` to the property. An overbroad schema is an "overpowered tool" โ the model has nothing to prevent it from calling the tool with input far beyond what the tool's prose contract
MCP tool input schema exposes an unconstrained string/any field with a risky name (command/query/sql/code/script/url/path/expr/ eval). Any caller can pass arbitrary values, which typically widens the tool's blast radius well beyond its intent. Narrow the schema with `.enum()`, `.regex()`, `.max()`, `Literal[...]`, Pydantic `Field(max_length=..., pattern=...)`, or a JSON Schema `enum` / `pattern` / `maxLength`.
Evidence
| 17 | getParams(): z.ZodRawShape { |
| 18 | return { |
| 19 | url: z.string().describe("URL to analyze"), |
| 20 | enable_javascript: z.boolean().optional().describe("Enable JavaScript rendering"), |
| 21 | custom_js: z.string().optional().describe("Custom JavaScript code to execute"), |
| 22 | custom_user_agent: z.string().optional().describe("Custom User-Agent header"), |
Remediation
Shape the schema to the tool's actual intent: - Zod: chain `.enum([...])`, `.regex(/.../)`, or `.max(n)`; prefer `z.enum([...])` or `z.literal(...)` when the value set is small. - Pydantic: use `Literal["a", "b"]` or `Field(max_length=..., pattern=r"...")`. - JSON Schema: add `"enum"`, `"pattern"`, or `"maxLength"` to the property. An overbroad schema is an "overpowered tool" โ the model has nothing to prevent it from calling the tool with input far beyond what the tool's prose contract
MCP tool input schema exposes an unconstrained string/any field with a risky name (command/query/sql/code/script/url/path/expr/ eval). Any caller can pass arbitrary values, which typically widens the tool's blast radius well beyond its intent. Narrow the schema with `.enum()`, `.regex()`, `.max()`, `Literal[...]`, Pydantic `Field(max_length=..., pattern=...)`, or a JSON Schema `enum` / `pattern` / `maxLength`.
Evidence
| 19 | getParams(): z.ZodRawShape { |
| 20 | return { |
| 21 | url: z.string().describe("URL of the page to parse"), |
| 22 | enable_javascript: z.boolean().optional().describe("Enable JavaScript rendering"), |
| 23 | custom_user_agent: z.string().optional().describe("Custom User-Agent header"), |
| 24 | accept_language: z.string().optional().describe("Accept-Language header value"), |
Remediation
Shape the schema to the tool's actual intent: - Zod: chain `.enum([...])`, `.regex(/.../)`, or `.max(n)`; prefer `z.enum([...])` or `z.literal(...)` when the value set is small. - Pydantic: use `Literal["a", "b"]` or `Field(max_length=..., pattern=r"...")`. - JSON Schema: add `"enum"`, `"pattern"`, or `"maxLength"` to the property. An overbroad schema is an "overpowered tool" โ the model has nothing to prevent it from calling the tool with input far beyond what the tool's prose contract
MCP tool input schema exposes an unconstrained string/any field with a risky name (command/query/sql/code/script/url/path/expr/ eval). Any caller can pass arbitrary values, which typically widens the tool's blast radius well beyond its intent. Narrow the schema with `.enum()`, `.regex()`, `.max()`, `Literal[...]`, Pydantic `Field(max_length=..., pattern=...)`, or a JSON Schema `enum` / `pattern` / `maxLength`.
Evidence
| 87 | name: 'dfs-keyword-content-gaps', |
| 88 | title: 'Analyze keyword optimization and content gaps', |
| 89 | params: { |
| 90 | url: z.string().describe('The URL of the page to analyze'), |
| 91 | keyword: z.string().describe('The primary keyword to optimize for'), |
| 92 | }, |
| 93 | handler: async (params) => { |
Remediation
Shape the schema to the tool's actual intent: - Zod: chain `.enum([...])`, `.regex(/.../)`, or `.max(n)`; prefer `z.enum([...])` or `z.literal(...)` when the value set is small. - Pydantic: use `Literal["a", "b"]` or `Field(max_length=..., pattern=r"...")`. - JSON Schema: add `"enum"`, `"pattern"`, or `"maxLength"` to the property. An overbroad schema is an "overpowered tool" โ the model has nothing to prevent it from calling the tool with input far beyond what the tool's prose contract
MCP tool input schema exposes an unconstrained string/any field with a risky name (command/query/sql/code/script/url/path/expr/ eval). Any caller can pass arbitrary values, which typically widens the tool's blast radius well beyond its intent. Narrow the schema with `.enum()`, `.regex()`, `.max()`, `Literal[...]`, Pydantic `Field(max_length=..., pattern=...)`, or a JSON Schema `enum` / `pattern` / `maxLength`.
Evidence
| 67 | name: 'dfs-internal-linking-audit', |
| 68 | title: 'Evaluate internal linking and crawl depth for better indexing', |
| 69 | params: { |
| 70 | url: z.string().describe('The URL of the page to analyze'), |
| 71 | }, |
| 72 | handler: async (params) => { |
| 73 | return { |
Remediation
Shape the schema to the tool's actual intent: - Zod: chain `.enum([...])`, `.regex(/.../)`, or `.max(n)`; prefer `z.enum([...])` or `z.literal(...)` when the value set is small. - Pydantic: use `Literal["a", "b"]` or `Field(max_length=..., pattern=r"...")`. - JSON Schema: add `"enum"`, `"pattern"`, or `"maxLength"` to the property. An overbroad schema is an "overpowered tool" โ the model has nothing to prevent it from calling the tool with input far beyond what the tool's prose contract
MCP tool input schema exposes an unconstrained string/any field with a risky name (command/query/sql/code/script/url/path/expr/ eval). Any caller can pass arbitrary values, which typically widens the tool's blast radius well beyond its intent. Narrow the schema with `.enum()`, `.regex()`, `.max()`, `Literal[...]`, Pydantic `Field(max_length=..., pattern=...)`, or a JSON Schema `enum` / `pattern` / `maxLength`.
Evidence
| 17 | getParams(): z.ZodRawShape { |
| 18 | return { |
| 19 | url: z.string().describe("URL of the page to parse"), |
| 20 | enable_javascript: z.boolean().optional().describe("Enable JavaScript rendering"), |
| 21 | custom_user_agent: z.string().optional().describe("Custom User-Agent header"), |
| 22 | accept_language: z.string().optional().describe("Accept-Language header value"), |
Remediation
Shape the schema to the tool's actual intent: - Zod: chain `.enum([...])`, `.regex(/.../)`, or `.max(n)`; prefer `z.enum([...])` or `z.literal(...)` when the value set is small. - Pydantic: use `Literal["a", "b"]` or `Field(max_length=..., pattern=r"...")`. - JSON Schema: add `"enum"`, `"pattern"`, or `"maxLength"` to the property. An overbroad schema is an "overpowered tool" โ the model has nothing to prevent it from calling the tool with input far beyond what the tool's prose contract
Package declares an install-time hook (npm postinstall/preinstall/prepare, setup.py cmdclass override, custom setuptools install class, or non-default pyproject build-backend). Anyone installing this package runs the hook. Confirm the hook is necessary and review its contents; prefer shipping a plain library without install-time execution.
Evidence
| 10 | "build": "tsc && node -e \"require('fs').chmodSync('build/main/main/cli.js', '755')\"", |
| 11 | "start": "node build/main/main/index.js", |
| 12 | "dev": "tsc --watch", |
| 13 | "prepare": "npm run build", |
| 14 | "http": "node build/main/main/index-http.js", |
| 15 | "sse": "node build/main/main/index-sse-http.js", |
| 16 | "cli": "node build/main/main/cli.js", |
Remediation
Prefer libraries that do not require install-time code execution: - Drop `postinstall`/`preinstall`/`prepare` scripts if the work can happen at runtime or build-time instead. - Ship pre-built native binaries rather than compiling via a custom `cmdclass` or `build_ext` override. - For Dockerfiles: replace `RUN curl โฆ | sh` with a pinned download + checksum verification + explicit `RUN` of a named script. - If the hook is unavoidable, document exactly what it does so downstream reviewers
Dockerfile never sets a non-root `USER` directive, so the CMD runs as root by default. Any RCE or library-level vulnerability exploited inside this container gets full privileges (MCP Top-10 R3). Add `USER <non-root>` before CMD / ENTRYPOINT in the final stage โ e.g. `USER 1000`, `USER nobody`, or `USER nonroot` on distroless.
Evidence
| 1 | FROM node:22-alpine |
| 2 | |
| 3 | # Set working directory |
| 4 | WORKDIR /app |
| 5 | |
| 6 | # Copy package files |
| 7 | COPY package*.json ./ |
| 8 | |
| 9 | # Install dependencies |
| 10 | # Use npm ci if lock file exists, else fallback to npm install |
| 11 | RUN if [ -f package-lock.json ]; then npm ci --ignore-scripts; else npm install --ignore-scripts; fi |
| 12 | |
| 13 | # Copy rest of the project |
| 14 | COPY tsconfig.json ./ |
| 15 | COPY src/ ./src/ |
| 16 | |
| 17 | # Build the project |
| 18 | RUN npm run build |
| 19 | |
| 20 | # Expose app port |
| 21 | EXPOSE 3000 |
| 22 | |
| 23 | # Set production environment |
| 24 | ENV NODE_ENV=production |
| 25 | |
| 26 | # Set entrypoint a |
Remediation
Create and switch to a non-root user before the CMD / ENTRYPOINT: RUN adduser --system --uid 1000 app USER 1000 Or reuse the base image's shipped non-root account (e.g. `USER nobody`, `USER nonroot` on distroless). Multi-stage builds only need the USER directive in the final stage.
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable โ a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 18 | uses: actions/checkout@v6 |
| 19 | |
| 20 | - name: Setup Node.js |
| 21 | uses: actions/setup-node@v6 |
| 22 | with: |
| 23 | node-version: '24' |
| 24 | registry-url: 'https://registry.npmjs.org' |
Remediation
Pin every `uses:` to a 40-character commit SHA. Trailing comment with the version helps reviewers: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v4.1.1` Automate the migration with `pinact` (https://github.com/suzuki-shunsuke/pinact) or `ratchet` (https://github.com/sethvargo/ratchet). Add a `pinact run --check` pre-commit hook so future PRs stay pinned. Re-pin when the action releases a new version โ Dependabot can do this automatically with `version-update-strategy: inc
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable โ a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 22 | echo "IMAGE_VERSION=${TAG_VERSION#v}" >> $GITHUB_ENV |
| 23 | |
| 24 | - name: Log in to Docker Hub |
| 25 | uses: docker/login-action@v3 |
| 26 | with: |
| 27 | username: ${{ secrets.DOCKER_USERNAME }} |
| 28 | password: ${{ secrets.DOCKER_TOKEN }} |
Remediation
Pin every `uses:` to a 40-character commit SHA. Trailing comment with the version helps reviewers: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v4.1.1` Automate the migration with `pinact` (https://github.com/suzuki-shunsuke/pinact) or `ratchet` (https://github.com/sethvargo/ratchet). Add a `pinact run --check` pre-commit hook so future PRs stay pinned. Re-pin when the action releases a new version โ Dependabot can do this automatically with `version-update-strategy: inc
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable โ a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 15 | environment: npm |
| 16 | steps: |
| 17 | - name: Checkout code |
| 18 | uses: actions/checkout@v6 |
| 19 | |
| 20 | - name: Setup Node.js |
| 21 | uses: actions/setup-node@v6 |
Remediation
Pin every `uses:` to a 40-character commit SHA. Trailing comment with the version helps reviewers: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v4.1.1` Automate the migration with `pinact` (https://github.com/suzuki-shunsuke/pinact) or `ratchet` (https://github.com/sethvargo/ratchet). Add a `pinact run --check` pre-commit hook so future PRs stay pinned. Re-pin when the action releases a new version โ Dependabot can do this automatically with `version-update-strategy: inc
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable โ a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 28 | password: ${{ secrets.DOCKER_TOKEN }} |
| 29 | |
| 30 | - name: Set up Docker Buildx |
| 31 | uses: docker/setup-buildx-action@v3 |
| 32 | |
| 33 | - name: Build and push |
| 34 | uses: docker/build-push-action@v6 |
Remediation
Pin every `uses:` to a 40-character commit SHA. Trailing comment with the version helps reviewers: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v4.1.1` Automate the migration with `pinact` (https://github.com/suzuki-shunsuke/pinact) or `ratchet` (https://github.com/sethvargo/ratchet). Add a `pinact run --check` pre-commit hook so future PRs stay pinned. Re-pin when the action releases a new version โ Dependabot can do this automatically with `version-update-strategy: inc
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable โ a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 31 | uses: docker/setup-buildx-action@v3 |
| 32 | |
| 33 | - name: Build and push |
| 34 | uses: docker/build-push-action@v6 |
| 35 | with: |
| 36 | context: . |
| 37 | platforms: linux/amd64,linux/arm64 |
Remediation
Pin every `uses:` to a 40-character commit SHA. Trailing comment with the version helps reviewers: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v4.1.1` Automate the migration with `pinact` (https://github.com/suzuki-shunsuke/pinact) or `ratchet` (https://github.com/sethvargo/ratchet). Add a `pinact run --check` pre-commit hook so future PRs stay pinned. Re-pin when the action releases a new version โ Dependabot can do this automatically with `version-update-strategy: inc
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable โ a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 14 | environment: dockerhub |
| 15 | steps: |
| 16 | - name: Checkout code |
| 17 | uses: actions/checkout@v4 |
| 18 | |
| 19 | - name: Set image version |
| 20 | run: | |
Remediation
Pin every `uses:` to a 40-character commit SHA. Trailing comment with the version helps reviewers: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v4.1.1` Automate the migration with `pinact` (https://github.com/suzuki-shunsuke/pinact) or `ratchet` (https://github.com/sethvargo/ratchet). Add a `pinact run --check` pre-commit hook so future PRs stay pinned. Re-pin when the action releases a new version โ Dependabot can do this automatically with `version-update-strategy: inc