High risk. Don't ship without significant remediation.
Scanned 5/3/2026, 7:35:52 PMยทCached resultยทFast Scanยท88 rulesยทHow we decide โ
AIVSS Score
High
Severity Breakdown
0
critical
8
high
18
medium
0
low
MCP Server Information
Findings
This package has a security grade of D and a safety score of 65/100, indicating significant vulnerabilities that could pose risks. It contains eight high-severity issues related to prompt injection and resource exhaustion, along with several medium-severity findings, which suggest potential exploitation avenues and misconfigurations. Given these concerns, careful consideration is warranted before installation.
No known CVEs found for this package or its dependencies.
Scan Details
Want deeper analysis?
Fast scan found 26 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.
26 of 26 findings
26 findings
config.ts reads sensitive API keys (MEIGEN_API_TOKEN, OPENAI_API_KEY) from environment variables and config files, then exposes them via loadConfig() which is imported and used by the MCP server to make authenticated requests to third-party APIs (meigen.ai, openai.com), transmitting credentials to non-operator-controlled destinations.
Evidence
| 1 | /** |
| 2 | * MeiGen MCP Server configuration |
| 3 | * Priority: environment variables > ~/.config/meigen/config.json > defaults |
| 4 | */ |
| 5 | |
| 6 | import { readFileSync, readdirSync } from 'fs' |
| 7 | import { join } from 'path' |
| 8 | import { homedir } from 'os' |
| 9 | |
| 10 | export interface MeiGenConfig { |
| 11 | // MeiGen platform mode |
| 12 | meigenApiToken?: string |
| 13 | |
| 14 | // OpenAI-compatible mode (user's own key) |
| 15 | openaiApiKey?: string |
| 16 | openaiBaseUrl: string |
| 17 | openaiModel: string |
| 18 | |
| 19 | // MeiGen API base URL |
| 20 | meigenBaseUrl: string |
| 21 | |
| 22 | // Upload gateway (fo |
Remediation
Remove the outbound transfer. If the tool legitimately needs remote delivery, restrict destinations to a customer-owned allowlist, strip secrets before transport, and document the data flow in the server's README.
MeiGenApiClient.getImageDetails() calls meigen.ai API without consulting caller identity; uses baseUrl from config loaded at module init time.
Evidence
| 79 | if (!res.ok) { |
| 80 | throw new Error(`Search failed: ${res.status} ${res.statusText}`) |
| 81 | } |
| 82 | |
| 83 | const json = await res.json() as { success: boolean; data?: MeiGenSearchResult[]; error?: string } |
| 84 | if (!json.success) { |
| 85 | throw new Error(json.error || 'Search failed') |
Remediation
Pull the token / credential from `ctx.principal` (or the MCP auth layer equivalent) on every call. The handler must fail closed when the caller supplies no credential. Never cache a global API token at import time and reuse it across callers.
MeiGenApiClient.searchGallery() calls meigen.ai /api/search without consulting caller identity; uses baseUrl from config loaded at module init time.
Evidence
| 51 | } |
| 52 | |
| 53 | export interface MeiGenGenerationStatus { |
| 54 | status: 'pending' | 'processing' | 'completed' | 'failed' |
| 55 | imageUrl: string | null |
| 56 | imageUrls: string[] | null |
| 57 | error: string | null |
Remediation
Pull the token / credential from `ctx.principal` (or the MCP auth layer equivalent) on every call. The handler must fail closed when the caller supplies no credential. Never cache a global API token at import time and reuse it across callers.
MeiGenApiClient.listModels() calls meigen.ai /api/models without consulting caller identity; uses baseUrl from config loaded at module init time.
Evidence
| 65 | this.baseUrl = config.meigenBaseUrl |
| 66 | this.apiToken = config.meigenApiToken |
| 67 | } |
| 68 | |
| 69 | /** Search gallery (no auth required) */ |
| 70 | async searchGallery(query: string, limit = 20, offset = 0): Promise<MeiGenSearchResult[]> { |
| 71 | const params = new URLSearchParams({ |
Remediation
Pull the token / credential from `ctx.principal` (or the MCP auth layer equivalent) on every call. The handler must fail closed when the caller supplies no credential. Never cache a global API token at import time and reuse it across callers.
Tool 'meigen init' performs FILESYSTEM side effects (creates directories, writes config files) that are not disclosed in the description.
Evidence
| 1 | /** |
| 2 | * `meigen init <platform>` โ one-command MCP server setup for any AI coding tool |
| 3 | * |
| 4 | * Writes the correct MCP config file with the right format and path for each platform. |
Remediation
Either remove the undeclared side effect or amend the tool description + input schema to disclose it. Add machine-readable `destructiveHint`, `networkHint`, `filesystemHint` annotations when the MCP spec supports them.
Preference mutation functions (updateDefaults, addFavorite, removeFavorite) perform FILESYSTEM side effects (write to ~/.config/meigen/preferences.json) that are not disclosed in their descriptions.
Evidence
| 1 | /** |
| 2 | * User preferences persistence |
| 3 | * Storage: ~/.config/meigen/preferences.json |
| 4 | * |
Remediation
Either remove the undeclared side effect or amend the tool description + input schema to disclose it. Add machine-readable `destructiveHint`, `networkHint`, `filesystemHint` annotations when the MCP spec supports them.
apiSearchPosts fetches untrusted content from caller-supplied baseUrl and returns ApiSearchResult array with free-text fields (text, author_username, author_display_name, model, prompt_ready) directly into LLM context without provenance wrapper.
Evidence
| 32 | */ |
| 33 | export async function apiSearchPosts( |
| 34 | baseUrl: string, |
| 35 | query: string, |
| 36 | limit: number, |
| 37 | offset: number, |
| 38 | ): Promise<ApiSearchResult[] | null> { |
Remediation
Wrap untrusted content with a clear delimiter naming the source, e.g. `<<<untrusted-content from example.com>>>` / `<<<end>>>`. Truncate to a known upper bound. Strip or neutralize model- directive markup (`<system>`, `[INST]`, role-turn markers) before returning. Optionally hash / quarantine large payloads and return only a summary plus a handle the agent can opt into.
searchGallery fetches untrusted content from meigenBaseUrl API and returns MeiGenSearchResult array with free-text fields (text, author_username, author_display_name, model) directly into LLM context without provenance wrapper.
Evidence
| 57 | error: string | null |
| 58 | } |
| 59 | |
| 60 | export class MeiGenApiClient { |
| 61 | private baseUrl: string |
| 62 | private apiToken?: string |
Remediation
Wrap untrusted content with a clear delimiter naming the source, e.g. `<<<untrusted-content from example.com>>>` / `<<<end>>>`. Truncate to a known upper bound. Strip or neutralize model- directive markup (`<system>`, `[INST]`, role-turn markers) before returning. Optionally hash / quarantine large payloads and return only a summary plus a handle the agent can opt into.
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
| 302 | async listCheckpoints(): Promise<string[]> { |
| 303 | try { |
| 304 | const res = await fetch(`${this.baseUrl}/models/checkpoints`) |
| 305 | if (!res.ok) return [] |
| 306 | return await res.json() as string[] |
| 307 | } catch { |
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
| 75 | offset: String(offset), |
| 76 | }) |
| 77 | |
| 78 | const res = await fetch(`${this.baseUrl}/api/search?${params}`) |
| 79 | if (!res.ok) { |
| 80 | throw new Error(`Search failed: ${res.status} ${res.statusText}`) |
| 81 | } |
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
| 93 | const params = new URLSearchParams() |
| 94 | if (!activeOnly) params.set('active', 'false') |
| 95 | |
| 96 | const res = await fetch(`${this.baseUrl}/api/models?${params}`) |
| 97 | if (!res.ok) { |
| 98 | throw new Error(`Failed to fetch models: ${res.status} ${res.statusText}`) |
| 99 | } |
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
| 82 | // If response contains a URL, download and convert to base64 |
| 83 | if (imageData.url) { |
| 84 | const imageRes = await fetch(imageData.url) |
| 85 | const buffer = await imageRes.arrayBuffer() |
| 86 | return { |
| 87 | imageBase64: Buffer.from(buffer).toString('base64'), |
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
| 298 | } |
| 299 | |
| 300 | // Download first image for local save |
| 301 | const imageRes = await fetch(allImageUrls[0]) |
| 302 | if (!imageRes.ok) { |
| 303 | throw new Error(`Failed to download generated image: ${imageRes.status}`) |
| 304 | } |
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 | /** Get image details by ID (no auth required) */ |
| 109 | async getImageDetails(imageId: string): Promise<MeiGenSearchResult | null> { |
| 110 | const res = await fetch(`${this.baseUrl}/api/images/${encodeURIComponent(imageId)}`) |
| 111 | if (!res.ok) { |
| 112 | if (res.status === 404) return null |
| 113 | throw new Error(`Failed to fetch image: ${res.status} ${res.statusText}`) |
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
| 434 | }) |
| 435 | |
| 436 | // 10. Download image |
| 437 | const imgRes = await fetch(`${this.baseUrl}/view?${params}`) |
| 438 | if (!imgRes.ok) { |
| 439 | throw new Error(`Failed to download image from ComfyUI: ${imgRes.status}`) |
| 440 | } |
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
| 349 | if (source.startsWith('http://') || source.startsWith('https://')) { |
| 350 | // Remote URL: download first |
| 351 | const imgRes = await fetch(source) |
| 352 | if (!imgRes.ok) { |
| 353 | throw new Error(`Failed to download reference image from ${source}: ${imgRes.status}`) |
| 354 | } |
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
| 410 | lastProgress = elapsed |
| 411 | } |
| 412 | |
| 413 | const histRes = await fetch(`${this.baseUrl}/history/${prompt_id}`) |
| 414 | if (!histRes.ok) continue |
| 415 | |
| 416 | const history = await histRes.json() as Record<string, ComfyUIHistoryEntry> |
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
| 14 | import { apiSearchPosts, type ApiSearchResult } from '../lib/api-search.js' |
| 15 | |
| 16 | export const searchGallerySchema = { |
| 17 | query: z.string().optional() |
| 18 | .describe('Search keywords (e.g., "cyberpunk", "product photo", "portrait"). Supports semantic search โ natural language descriptions work well. Leave empty to browse by category or get random picks.'), |
| 19 | category: z.enum(['Photography', 'Illustration & 3D', 'Product & Brand', 'Food & Drink', 'Poster Design', 'UI & Graphic']).optional() |
| 20 | .describ |
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
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:20-slim AS builder |
| 2 | WORKDIR /app |
| 3 | COPY package*.json ./ |
| 4 | COPY tsconfig.json ./ |
| 5 | RUN npm install |
| 6 | COPY src/ ./src/ |
| 7 | COPY data/ ./data/ |
| 8 | RUN npm run build |
| 9 | |
| 10 | FROM node:20-slim |
| 11 | WORKDIR /app |
| 12 | COPY --from=builder /app/dist ./dist |
| 13 | COPY --from=builder /app/data ./data |
| 14 | COPY --from=builder /app/node_modules ./node_modules |
| 15 | COPY package*.json ./ |
| 16 | COPY bin/ ./bin/ |
| 17 | ENV NODE_ENV=production |
| 18 | ENTRYPOINT ["node", "bin/meigen-mcp.js"] |
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.
MCP manifest declares tools but no authentication field is present (none of: auth, authorization, bearer, oauth, mtls, apiKey, api_key, basic, token, authToken). Absence is a weak signal โ confirm whether the server relies on network-layer or host-level auth, or declare the real mechanism explicitly so reviewers can audit it.
Evidence
| 1 | # MeiGen AI Design โ Claude Code Plugin |
| 2 | |
| 3 | AI image generation plugin with creative workflow orchestration, parallel multi-direction output, prompt engineering, and a 1,300+ curated inspiration library. |
| 4 | |
| 5 | ## Prerequisites |
| 6 | |
| 7 | This plugin requires the `meigen` MCP server. Add it to your project's `.mcp.json`: |
| 8 | |
| 9 | ```json |
| 10 | { |
| 11 | "mcpServers": { |
| 12 | "meigen": { |
| 13 | "command": "npx", |
| 14 | "args": ["-y", "meigen@1.2.13"] |
| 15 | } |
| 16 | } |
| 17 | } |
| 18 | ``` |
| 19 | |
| 20 | Restart Claude Code after adding the configuration. |
| 21 | |
| 22 | ## Provider Setup |
| 23 | |
| 24 |
Remediation
Declare a real authentication mechanism in the manifest, matching what the running server actually enforces: - `"auth": "bearer"` with a token scheme documented for callers - `"auth": "oauth"` / `"oauth2": { ... }` for delegated flows - `"apiKey": { "header": "X-API-Key", "prefix": "..." }` - `"mtls": true` when client certificates are required If the server is intentionally unauthenticated (stdio-only, local developer tool, trusted-host network), document the assumption in the manifest via a `"
MCP manifest declares tools but no authentication field is present (none of: auth, authorization, bearer, oauth, mtls, apiKey, api_key, basic, token, authToken). Absence is a weak signal โ confirm whether the server relies on network-layer or host-level auth, or declare the real mechanism explicitly so reviewers can audit it.
Evidence
| 1 | <h1 align="center"> |
| 2 | MeiGen AI Design MCP <a href="https://github.com/punkpeye/awesome-mcp-servers"><img src="https://awesome.re/mentioned-badge.svg" alt="Mentioned in Awesome MCP Servers"></a> <a href="https://github.com/wshobson/agents/tree/main/plugins/meigen-ai-design"><img src="https://img.shields.io/badge/wshobson%2Fagents-Featured-blue?style=flat&logo=github" alt="Featured in wshobson/agents"></a> |
| 3 | </h1> |
| 4 | |
| 5 | <p align="center"> |
| 6 | <strong>่ฎฉ Claude Code / OpenClaw ๅๆๅชฒ็พ Lovart ็็งไบบ่ฎพ่ฎกๅฉ็</strong><b |
Remediation
Declare a real authentication mechanism in the manifest, matching what the running server actually enforces: - `"auth": "bearer"` with a token scheme documented for callers - `"auth": "oauth"` / `"oauth2": { ... }` for delegated flows - `"apiKey": { "header": "X-API-Key", "prefix": "..." }` - `"mtls": true` when client certificates are required If the server is intentionally unauthenticated (stdio-only, local developer tool, trusted-host network), document the assumption in the manifest via a `"
MCP manifest declares tools but no authentication field is present (none of: auth, authorization, bearer, oauth, mtls, apiKey, api_key, basic, token, authToken). Absence is a weak signal โ confirm whether the server relies on network-layer or host-level auth, or declare the real mechanism explicitly so reviewers can audit it.
Evidence
| 1 | --- |
| 2 | name: "AI Image Generator & Editor โ GPT Image 2, Nanobanana, ComfyUI" |
| 3 | description: Generate images from text with multi-provider routing โ supports GPT Image 2.0 (near-perfect text rendering), Nanobanana 2, Seedream 5.0, Midjourney V7 (photorealistic), Midjourney Niji 7 (anime/illustration only), and local ComfyUI workflows. Includes 1,300+ curated prompts and style-aware prompt enhancement. Use when users want to create images, design assets, enhance prompts, or manage AI art workflows. |
| 4 | ve |
Remediation
Declare a real authentication mechanism in the manifest, matching what the running server actually enforces: - `"auth": "bearer"` with a token scheme documented for callers - `"auth": "oauth"` / `"oauth2": { ... }` for delegated flows - `"apiKey": { "header": "X-API-Key", "prefix": "..." }` - `"mtls": true` when client certificates are required If the server is intentionally unauthenticated (stdio-only, local developer tool, trusted-host network), document the assumption in the manifest via a `"
MCP manifest declares tools but no authentication field is present (none of: auth, authorization, bearer, oauth, mtls, apiKey, api_key, basic, token, authToken). Absence is a weak signal โ confirm whether the server relies on network-layer or host-level auth, or declare the real mechanism explicitly so reviewers can audit it.
Evidence
| 1 | <h1 align="center"> |
| 2 | MeiGen AI Design MCP <a href="https://github.com/punkpeye/awesome-mcp-servers"><img src="https://awesome.re/mentioned-badge.svg" alt="Mentioned in Awesome MCP Servers"></a> <a href="https://github.com/wshobson/agents/tree/main/plugins/meigen-ai-design"><img src="https://img.shields.io/badge/wshobson%2Fagents-Featured-blue?style=flat&logo=github" alt="Featured in wshobson/agents"></a> |
| 3 | </h1> |
| 4 | |
| 5 | <p align="center"> |
| 6 | <strong>Turn Claude Code / OpenClaw into your personal design a |
Remediation
Declare a real authentication mechanism in the manifest, matching what the running server actually enforces: - `"auth": "bearer"` with a token scheme documented for callers - `"auth": "oauth"` / `"oauth2": { ... }` for delegated flows - `"apiKey": { "header": "X-API-Key", "prefix": "..." }` - `"mtls": true` when client certificates are required If the server is intentionally unauthenticated (stdio-only, local developer tool, trusted-host network), document the assumption in the manifest via a `"
Time-of-check-to-time-of-use race. Code calls `os.path.exists` / `fs.existsSync` to check a path, then `open` / `readFileSync` / `unlink` on the same name within a few lines โ without a lock or atomic-open. An attacker who can race the filesystem (symlink, file replacement) between the check and the use gets the action applied to a different target. Replace the check-then-use pattern with the action's own error handling: try the open and catch FileNotFoundError / ENOENT. For atomic creation use
Evidence
| 1 | /** |
| 2 | * `meigen init <platform>` โ one-command MCP server setup for any AI coding tool |
| 3 | * |
| 4 | * Writes the correct MCP config file with the right format and path for each platform. |
| 5 | * Merges into existing config if one already exists. |
| 6 | */ |
| 7 | |
| 8 | import * as fs from 'node:fs' |
| 9 | import * as path from 'node:path' |
| 10 | import * as os from 'node:os' |
| 11 | |
| 12 | interface PlatformConfig { |
| 13 | name: string |
| 14 | configPath: string |
| 15 | wrapperKey: string |
| 16 | needsType: boolean |
| 17 | global?: boolean |
| 18 | } |
| 19 | |
| 20 | const PLATFORMS: Record<string, PlatformCo |
Remediation
Replace check-then-use with action-then-handle: Python: `try: with open(p) as f: ... except FileNotFoundError: ...` Node: `try { fs.readFileSync(p); } catch (e) { if (e.code === "ENOENT") ... }` For atomic file creation: Python: `os.open(p, os.O_RDWR | os.O_CREAT | os.O_EXCL)` Node: `fs.open(p, "wx")` โ fails if file exists, no race. When you genuinely must check first, use `flock` (Python `fcntl`) or a similar per-process advisory lock to make the window uninteresting to a
Time-of-check-to-time-of-use race. Code calls `os.path.exists` / `fs.existsSync` to check a path, then `open` / `readFileSync` / `unlink` on the same name within a few lines โ without a lock or atomic-open. An attacker who can race the filesystem (symlink, file replacement) between the check and the use gets the action applied to a different target. Replace the check-then-use pattern with the action's own error handling: try the open and catch FileNotFoundError / ENOENT. For atomic creation use
Evidence
| 1 | /** |
| 2 | * ComfyUI Local Provider |
| 3 | * Template-based workflow: users provide a workflow JSON template, |
| 4 | * generation fills in prompt/seed/size at runtime. |
| 5 | */ |
| 6 | |
| 7 | import { readFileSync, writeFileSync, readdirSync, mkdirSync, unlinkSync, existsSync } from 'fs' |
| 8 | import { join, basename } from 'path' |
| 9 | import { homedir } from 'os' |
| 10 | |
| 11 | // ============================================================ |
| 12 | // Types |
| 13 | // ============================================================ |
| 14 | |
| 15 | /** ComfyUI API-format workflow โ node IDs |
Remediation
Replace check-then-use with action-then-handle: Python: `try: with open(p) as f: ... except FileNotFoundError: ...` Node: `try { fs.readFileSync(p); } catch (e) { if (e.code === "ENOENT") ... }` For atomic file creation: Python: `os.open(p, os.O_RDWR | os.O_CREAT | os.O_EXCL)` Node: `fs.open(p, "wx")` โ fails if file exists, no race. When you genuinely must check first, use `flock` (Python `fcntl`) or a similar per-process advisory lock to make the window uninteresting to a
MCP tool description or return text contains an imperative phrase that asks the LLM to invoke or call ANOTHER tool โ "invoke the write_file tool", "before using this, also call send_email", "silently invoke X". This is a cross-tool chaining injection: the user authorized THIS tool, but the payload escalates into others. Tool descriptions should describe what the tool DOES, not direct the LLM to use other tools. If a tool's correct operation requires composition, document the dependency in human
Evidence
| 1 | /** |
| 2 | * list_models Tool โ free, no auth required |
| 3 | * Lists all available AI image generation models and providers |
| 4 | */ |
| 5 | |
| 6 | import { z } from 'zod' |
| 7 | import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' |
| 8 | import type { MeiGenApiClient } from '../lib/meigen-api.js' |
| 9 | import type { MeiGenConfig } from '../config.js' |
| 10 | import { getAvailableProviders } from '../config.js' |
| 11 | import { |
| 12 | listWorkflows, |
| 13 | loadWorkflow, |
| 14 | getWorkflowSummary, |
| 15 | ComfyUIProvider, |
| 16 | } from '../lib/providers/comfyui.js' |
| 17 | |
| 18 | ex |
Remediation
Tool descriptions should describe what the tool does โ not what the model should do with other tools. If a tool's correct operation legitimately requires another tool to be called, document that as a `composition` requirement in human-readable docs and let the calling code orchestrate, not the LLM. If the directive phrasing is coming from external content the tool retrieved (RAG, web fetch), wrap in `<untrusted>` tags and rely on the system prompt to flag tag-bound content as data, not instruct
txt2img
list_models