Use with caution. Address findings before production.
Scanned 5/3/2026, 7:00:03 PM·Cached result·Fast Scan·45 rules·How we decide ↗
AIVSS Score
Medium
Severity Breakdown
0
critical
4
high
16
medium
3
low
MCP Server Information
Findings
This package has a concerning security profile with a C grade and a safety score of 66/100, indicating moderate risk. It contains four high-severity issues—including insecure deserialization and ANSI escape injection vulnerabilities—and 16 medium-severity findings, mostly tied to server configuration and resource exhaustion risks. While no critical flaws were detected, the volume of high and medium issues suggests potential stability or security gaps that could expose your system if not addressed.
No known CVEs found for this package or its dependencies.
Scan Details
Want deeper analysis?
Fast scan found 23 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.
23 of 23 findings
23 findings
Unsafe deserialization primitive detected. pickle.load(s), yaml.load (without SafeLoader), marshal.load(s), and shelve.open execute arbitrary code when the input is attacker-controlled.
Evidence
| 8 | export function parseWorkflow(filePath: string): WorkflowDefinition { |
| 9 | const raw = readFileSync(filePath, 'utf-8'); |
| 10 | const doc = yaml.load(raw) as Record<string, unknown>; |
| 11 | |
| 12 | // 基本校验 |
| 13 | if (!doc || typeof doc !== 'object') { |
Remediation
Replace pickle with json/msgpack or a schema-validated format (protobuf, cap'n proto). Use yaml.safe_load instead of yaml.load. Never deserialize data from an untrusted source with these APIs.
Unsafe deserialization primitive detected. pickle.load(s), yaml.load (without SafeLoader), marshal.load(s), and shelve.open execute arbitrary code when the input is attacker-controlled.
Evidence
| 218 | return files.map(f => { |
| 219 | try { |
| 220 | const content = readFileSync(f, 'utf-8'); |
| 221 | const doc = yaml.load(content) as Record<string, unknown>; |
| 222 | return { |
| 223 | file: relative(process.cwd(), f), |
| 224 | name: (doc?.name as string) || '(unnamed)', |
Remediation
Replace pickle with json/msgpack or a schema-validated format (protobuf, cap'n proto). Use yaml.safe_load instead of yaml.load. Never deserialize data from an untrusted source with these APIs.
Unsafe deserialization primitive detected. pickle.load(s), yaml.load (without SafeLoader), marshal.load(s), and shelve.open execute arbitrary code when the input is attacker-controlled.
Evidence
| 65 | return files.map(f => { |
| 66 | try { |
| 67 | const content = readFileSync(f, 'utf-8'); |
| 68 | const doc = yaml.load(content) as Record<string, unknown>; |
| 69 | return { |
| 70 | file: relative(process.cwd(), f), |
| 71 | name: (doc?.name as string) || '(unnamed)', |
Remediation
Replace pickle with json/msgpack or a schema-validated format (protobuf, cap'n proto). Use yaml.safe_load instead of yaml.load. Never deserialize data from an untrusted source with these APIs.
TLS certificate verification is disabled on an outbound HTTP client. Any MITM in the network path can intercept and modify requests / responses — credentials, tokens, and tool output flow over a channel with no integrity guarantee. Python requests / httpx: drop `verify=False`. If the peer is using a private CA, set `verify="/path/to/ca-bundle.pem"` or configure the system trust store. Node TS axios / fetch: drop `rejectUnauthorized: false` from the agent / `httpsAgent` options. Same private-CA
Evidence
| 328 | - name: Deploy to Splunk |
| 329 | run: | |
| 330 | # Push compiled rules via Splunk REST API |
| 331 | curl -k -u "${{ secrets.SPLUNK_USER }}:${{ secrets.SPLUNK_PASS }}" \ |
| 332 | https://${{ secrets.SPLUNK_HOST }}:8089/servicesNS/admin/search/saved/searches \ |
| 333 | -d @compiled/splunk/rules.conf |
Remediation
Drop the verify-disable flag. If the peer presents a private CA: - Python: pass `verify="/path/to/ca.pem"` or trust the system store - Node: `new https.Agent({ ca: fs.readFileSync("ca.pem") })` - Go: load the CA via `x509.NewCertPool().AppendCertsFromPEM(...)` and set `tls.Config.RootCAs` Self-signed certificates: import the cert into the OS trust chain rather than disabling verification per-call.
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
| 626 | if (args[i] === '--input' || args[i] === '-i') { |
| 627 | const pair = args[++i]; |
| 628 | if (!pair) { |
| 629 | console.error('--input 需要 key=value 参数'); |
| 630 | process.exit(1); |
| 631 | } |
| 632 | const eqIdx = pair.indexOf('='); |
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
| 631 | } |
| 632 | const eqIdx = pair.indexOf('='); |
| 633 | if (eqIdx < 1) { |
| 634 | console.error(`无效的 input 格式: ${pair} (应为 key=value)`); |
| 635 | process.exit(1); |
| 636 | } |
| 637 | const key = pair.slice(0, eqIdx); |
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
| 95 | async function handleRun(): Promise<void> { |
| 96 | const filePath = args[1]; |
| 97 | if (!filePath) { |
| 98 | console.error('用法: ao run <workflow.yaml> [--input key=value ...]'); |
| 99 | process.exit(1); |
| 100 | } |
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
| 334 | app.use('/webhook/event', lark.adaptExpress(eventDispatcher)); |
| 335 | app.use('/webhook/card', lark.adaptExpress(cardActionHandler)); |
| 336 | |
| 337 | app.listen(3000, () => console.log('Feishu event service started')); |
| 338 | ``` |
| 339 | |
| 340 | ### Bitable Operations |
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
| 173 | "-af", "loudnorm=I=-16:TP=-1.5:LRA=11", # EBU R128 loudness normalization |
| 174 | output_path |
| 175 | ] |
| 176 | subprocess.run(cmd, check=True, capture_output=True) |
| 177 | return output_path |
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
| 55 | const controller = new AbortController(); |
| 56 | const timeout = setTimeout(() => controller.abort(), 2000); |
| 57 | const ollamaUrl = process.env.OLLAMA_BASE_URL || 'http://localhost:11434'; |
| 58 | const res = await fetch(`${ollamaUrl.replace(/\/+$/, '')}/api/tags`, { |
| 59 | signal: controller.signal, |
| 60 | }); |
| 61 | clearTimeout(timeout); |
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
| 99 | """Fetch and parse raw messages, preserving full MIME structure.""" |
| 100 | messages = [] |
| 101 | for msg_id in thread_ids: |
| 102 | _, data = imap_conn.fetch(msg_id, "(RFC822)") |
| 103 | raw = data[0][1] |
| 104 | parsed = email.message_from_bytes(raw, policy=policy.default) |
| 105 | messages.append({ |
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
| 227 | 'plan_workflow', |
| 228 | 'Show the DAG execution plan for a workflow', |
| 229 | { |
| 230 | path: z.string().describe('Path to workflow YAML file'), |
| 231 | }, |
| 232 | async ({ path: workflowPath }) => { |
| 233 | try { |
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
| 296 | 'validate_workflow', |
| 297 | 'Validate a workflow YAML without executing', |
| 298 | { |
| 299 | path: z.string().describe('Path to workflow YAML file'), |
| 300 | }, |
| 301 | async ({ path: workflowPath }) => { |
| 302 | try { |
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
| 160 | 'validate_workflow', |
| 161 | 'Validate a workflow YAML without executing', |
| 162 | { |
| 163 | path: z.string().describe('Path to workflow YAML file'), |
| 164 | }, |
| 165 | async ({ path: workflowPath }) => { |
| 166 | try { |
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
| 116 | 'run_workflow', |
| 117 | 'Execute a YAML workflow with the DAG engine', |
| 118 | { |
| 119 | path: z.string().describe('Path to workflow YAML file'), |
| 120 | inputs: z.record(z.string(), z.string()).optional().describe('Key-value input variables'), |
| 121 | provider: z.enum(['claude-code', 'gemini-cli', 'copilot-cli', 'codex-cli', 'openclaw-cli', 'hermes-cli', 'deepseek', 'claude', 'openai', 'ollama']).optional().describe('Override LLM provider'), |
| 122 | model: z.string().optional().describe('Override model name'), |
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
| 253 | 'run_workflow', |
| 254 | 'Execute a YAML workflow with the DAG engine', |
| 255 | { |
| 256 | path: z.string().describe('Path to workflow YAML file'), |
| 257 | inputs: z.record(z.string()).optional().describe('Key-value input variables'), |
| 258 | provider: z.enum(['deepseek', 'claude', 'openai', 'ollama']).optional().describe('Override LLM provider'), |
| 259 | model: z.string().optional().describe('Override model name'), |
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
| 363 | 'plan_workflow', |
| 364 | 'Show the DAG execution plan for a workflow', |
| 365 | { |
| 366 | path: z.string().describe('Path to workflow YAML file'), |
| 367 | }, |
| 368 | async ({ path: workflowPath }) => { |
| 369 | try { |
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
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
| 23 | name: Validate agent frontmatter and structure |
| 24 | runs-on: ubuntu-latest |
| 25 | steps: |
| 26 | - uses: actions/checkout@v4 |
| 27 | with: |
| 28 | fetch-depth: 0 |
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
| 17 | - uses: actions/checkout@v4 |
| 18 | |
| 19 | - name: Setup Node.js ${{ matrix.node-version }} |
| 20 | uses: actions/setup-node@v4 |
| 21 | with: |
| 22 | node-version: ${{ matrix.node-version }} |
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 | node-version: [20, 22] |
| 15 | |
| 16 | steps: |
| 17 | - uses: actions/checkout@v4 |
| 18 | |
| 19 | - name: Setup Node.js ${{ matrix.node-version }} |
| 20 | uses: actions/setup-node@v4 |
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
Silent error swallowing detected. An except clause that does pass or ... discards the exception with no log, no metric, and no trace. This blinds incident response and hides real failures.
Evidence
| 69 | killed = true; |
| 70 | child.kill('SIGTERM'); |
| 71 | // SIGTERM 后 5s 仍未退出则强制 SIGKILL,防止僵尸进程 |
| 72 | setTimeout(() => { try { child.kill('SIGKILL'); } catch {} }, 5000); |
| 73 | }, timeout) |
| 74 | : null; |
Remediation
Log the exception at minimum (`logger.exception(e)`), emit a metric, or re-raise if the error is not recoverable. If you genuinely want to ignore an exception, say so with a comment.
Silent error swallowing detected. An except clause that does pass or ... discards the exception with no log, no metric, and no trace. This blinds incident response and hides real failures.
Evidence
| 40 | return await this._exec(args, userMessage, timeout); |
| 41 | } finally { |
| 42 | if (systemPromptFile) { |
| 43 | try { unlinkSync(systemPromptFile); } catch {} |
| 44 | } |
| 45 | } |
| 46 | } |
Remediation
Log the exception at minimum (`logger.exception(e)`), emit a metric, or re-raise if the error is not recoverable. If you genuinely want to ignore an exception, say so with a comment.
Silent error swallowing detected. An except clause that does pass or ... discards the exception with no log, no metric, and no trace. This blinds incident response and hides real failures.
Evidence
| 62 | const timer = setTimeout(() => { |
| 63 | killed = true; |
| 64 | child.kill('SIGTERM'); |
| 65 | setTimeout(() => { try { child.kill('SIGKILL'); } catch {} }, 5000); |
| 66 | }, timeout); |
| 67 | |
| 68 | child.stdout!.on('data', (chunk: Buffer) => { |
Remediation
Log the exception at minimum (`logger.exception(e)`), emit a metric, or re-raise if the error is not recoverable. If you genuinely want to ignore an exception, say so with a comment.