Back to skill

Security audit

Ask Council

Security checks for vulnerabilities and agentic risk

Overview

This skill largely does what it says, but it sends questions through a shell script to a backend and prints untrusted model output without terminal sanitization, so it should be reviewed before installation.

Install only if you trust the local LLM Council backend and any models or providers it uses. Prefer invoking it explicitly with /council, avoid sending secrets or sensitive personal data, and consider fixing terminal-output sanitization before regular use.

Vulnerability Patterns
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
  • Embedded Malicious CodeShips malicious scripts inside the skill and executes them locally
Findings (1)

T09 · Insecure Skill Coding Practices

Warning
Location
ask-council.sh:75
Finding

Unsanitized API and Model Output Allows Terminal Escape-Sequence Injection

Content
View full analysis
&2; exit 1; } ``` The synthesized model response is also printed without sanitization: ```bash CHAIRMAN_ANSWER=$(echo "$STATUS" | python3 -c " import sys, json data = json.load(sys.stdin) stage3 = data.get('stage3', {}) if stage3.get('status') == 'complete': result = stage3.get('data', {}) print(result.get('response', 'No answer found')) else: print('Chairman did not complete synthesis') ") ``` It is subsequently emitted to the terminal: ```bash echo "$CHAIRMAN_ANSWER" ``` ### Technical Analysis The script treats the local API response and model-generated chairman answer as trusted terminal text. Python's JSON parser converts encoded control characters such as `\u001b` into actual control bytes. Those bytes are then written directly to the user's terminal. The error path is additionally exposed because `error()` uses `echo -e`, which interprets backslash escape sequences in the supplied error message. Consequently, even text containing encoded escape notation may be transformed into terminal control characters. Although the API is bound to `127.0.0.1`, its output is not inherently trustworthy. It incorporates content produced by language models and could also be controlled by a compromised, replaced, or malicious local backend. ### Attack Path 1. An attacker influences a council model response through crafted question content, compromised model output, or control of the local backend. 2. The backend returns a `stage3.data.response` or `error` value c ...[truncated 1153 chars]
Remediation
View remediation
&2 exit 1 } ``` 2. Sanitize all API and model-generated output before writing it to a terminal. Remove C0/C1 control characters while preserving only explicitly required characters such as newline and tab. 3. Perform sanitization after JSON decoding, because encoded JSON sequences are converted into actual control characters during parsing. For example, process the response with Python before printing: ```python import re safe_text = re.sub(r'[\x00-\x08\x0b-\x1f\x7f-\x9f]', '', untrusted_text) print(safe_text) ``` 4. Use `printf '%s\n' "$CHAIRMAN_ANSWER"` rather than `echo "$CHAIRMAN_ANSWER"` for predictable literal output. 5. Apply the same handling to backend error strings, conversation identifiers, and any future API-controlled values displayed in the terminal. 6. If formatting must be retained, use an allowlist-based renderer rather than permitting arbitrary ANSI sequences. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
Findings (11)

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding

The documented behavior frames the skill as a simple Q&A helper, but it also sends user prompts to a backend service and exposes a local network web UI link. This mismatch reduces informed consent, may leak sensitive user queries to another service, and reveals internal network details that should not be disclosed by default.

Content

No source excerpt is available for this finding.

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

Scanner excerpt · SKILL.md (reported line 90)May include surrounding context.

md
| `SKILL.md` | Documentation |

External Script Fetching

High
Category
Supply Chain
Confidence
90% confidence
Finding

Remote code is downloaded and executed. This bypasses code review and could introduce malicious code.

Content

Scanner excerpt · ask-council.sh (reported line 25)May include surrounding context.

sh
fi

# ── Check if backend is running ───────────────────────────────────────────────
if ! curl -s "$API_BASE/" > /dev/null 2>&1; then
  error "LLM Council backend not running. Start it first:\n  /install-llm-council"
fi

External Script Fetching

High
Category
Supply Chain
Confidence
90% confidence
Finding

Remote code is downloaded and executed. This bypasses code review and could introduce malicious code.

Content

Scanner excerpt · ask-council.sh (reported line 38)May include surrounding context.

sh
fi

# ── Start council run ─────────────────────────────────────────────────────────
RUN_RESPONSE=$(curl -s -X POST "$API_BASE/api/conversations/$CONVO_ID/runs" \
  -H "Content-Type: application/json" \
  -d "{\"content\": $(echo "$QUESTION" | python3 -c 'import json, sys; print(json.dumps(sys.stdin.read().strip()))')}")
RUN_ID=$(echo "$RUN_RESPONSE" | python3 -c "import sys, json; print(json.load(sys.stdin)['run_id'])")

External Script Fetching

High
Category
Supply Chain
Confidence
90% confidence
Finding

Remote code is downloaded and executed. This bypasses code review and could introduce malicious code.

Content

Scanner excerpt · ask-council.sh (reported line 58)May include surrounding context.

sh
error "Timed out after ${TIMEOUT_SEC}s. The council is still talking."
  fi

  STATUS=$(curl -s "$API_BASE/api/conversations/$CONVO_ID/runs/$RUN_ID")
  RUN_STATE=$(echo "$STATUS" | python3 -c "import sys, json; print(json.load(sys.stdin).get('status', 'unknown'))")
  
  case "$RUN_STATE" in

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
94% confidence
Finding

The skill instructs the agent to execute a shell script but does not declare any tool scope or permissions, creating an authorization and transparency gap. This makes it easier for a user or reviewer to underestimate the skill's capabilities and increases the risk of unintended command execution through the agent.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
96% confidence
Finding

The skill description does not warn users that their question will be sent to an external or separate backend service. This undermines informed consent and can lead to accidental disclosure of sensitive data users believed would remain local to the chat environment.

Content

No source excerpt is available for this finding.

Vague Triggers

Medium
Category
Not specified by scanner
Confidence
94% confidence
Finding

The trigger phrase "ask council" is broad enough to match normal conversation, which can cause the skill to activate unintentionally. Accidental activation can result in unintended shell execution and transmission of a user's text to the council backend without clear intent.

Content

No source excerpt is available for this finding.

External Transmission

Medium
Category
Data Exfiltration
Confidence
70% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · ask-council.sh (reported line 25)May include surrounding context.

sh
fi

# ── Check if backend is running ───────────────────────────────────────────────
if ! curl -s "$API_BASE/" > /dev/null 2>&1; then
  error "LLM Council backend not running. Start it first:\n  /install-llm-council"
fi

External Transmission

Medium
Category
Data Exfiltration
Confidence
97% confidence
Finding

This POST request transmits the user's full question to the backend service for processing. In skill context, that is the intended function, but it is still a real data-transmission behavior with privacy implications because prompts may contain secrets and the backend may persist, inspect, or relay them to other services or models.

Content

Scanner excerpt · ask-council.sh (reported line 38)May include surrounding context.

sh
fi

# ── Start council run ─────────────────────────────────────────────────────────
RUN_RESPONSE=$(curl -s -X POST "$API_BASE/api/conversations/$CONVO_ID/runs" \
  -H "Content-Type: application/json" \
  -d "{\"content\": $(echo "$QUESTION" | python3 -c 'import json, sys; print(json.dumps(sys.stdin.read().strip()))')}")
RUN_ID=$(echo "$RUN_RESPONSE" | python3 -c "import sys, json; print(json.load(sys.stdin)['run_id'])")

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The script sends the user-supplied QUESTION to a backend service via HTTP without any explicit warning, confirmation, or disclosure at the point of use. Even though the destination is localhost, the content leaves the shell process and may be logged, retained, or further relayed by the backend or underlying models, which is a privacy and data-handling risk for users who may assume the question stays local to the CLI.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.