Back to skill

Security audit

AFOL Brickset

Security checks for vulnerabilities and agentic risk

Overview

The skill is mostly coherent for Brickset lookups and collection management, but its CLI can send Brickset secrets to an arbitrary base URL if that option or environment variable is changed.

Review this before installing if you will use real Brickset credentials. Only use the official Brickset API URL, avoid setting BRICKSET_BASE_URL or passing --base-url, and confirm account-changing actions before allowing the CLI to run with --yes. Treat BRICKSET_API_KEY, BRICKSET_USER_HASH, BRICKSET_USERNAME, and BRICKSET_PASSWORD as secrets.

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

Error
Location
scripts/brickset_cli.py:30
Finding
Arbitrary API Base URL Allows Credential Exfiltration## Vulnerability Details **File Location**: `scripts/brickset_cli.py:30-56, 109, 181-182, 235-239` **Vulnerability Type**: Unrestricted credential-bearing network destination **Risk Level**: High The CLI permits the API base URL to be supplied through `--base-url` or `BRICKSET_BASE_URL` without validating the scheme, hostname, port, query, or fragment. It then automatically transmits Brickset credentials to that destination. Relevant code: ```python class BricksetClient: def __init__(self, api_key: str, user_hash: str = "", base_url: str = DEFAULT_BASE_URL, timeout: int = READ_TIMEOUT_SECONDS): self.api_key = api_key self.user_hash = user_hash self.base_url = base_url.rstrip("/") self.timeout = timeout def post_form(self, path: str, fields: dict[str, Any], *, user_hash: str | None = None) -> Any: body_fields = { "apiKey": self.api_key, "userHash": self.user_hash if user_hash is None else user_hash, **clean_params(fields), } body = urllib.parse.urlencode(body_fields, doseq=True).encode() url = f"{self.base_url}{path}" headers = {"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"} return self._request("POST", url, body, headers) def login(self, username: str, password: str) -> Any: body = urllib.parse.urlencode({"apiKey": self.api_key, "username": username, "password": password}).encode() url = f"{self.base_url}/login" headers = {"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"} return self._request("POST", url, body, headers) def _request(self, method: str, url: str, body: bytes | None = None, headers: dict[str, str] | None = None) -> Any: request = urllib.request.Request(url, data=body, headers=headers or {"Accept": "application/json"}, method=method) try: ...[truncated 3277 chars]
Remediation
## Remediation Suggestions 1. Remove the production base-URL override and always use `https://brickset.com/api/v3.asmx`. 2. If an override is operationally necessary, parse it with `urllib.parse.urlsplit()` and enforce: - Scheme exactly equal to `https`. - Hostname exactly equal to `brickset.com`. - Expected API path only. - No embedded username or password. - No query string or fragment. - No unexpected port. 3. Resolve endpoint paths against a validated origin rather than using direct string concatenation. 4. Do not load or transmit `BRICKSET_USER_HASH` for public operations unless the operation explicitly requires authenticated context. 5. For local testing, require a conspicuous development-only switch and refuse to send non-placeholder credentials to custom endpoints. 6. Add automated tests confirming that HTTP URLs, non-Brickset hosts, lookalike domains, embedded credentials, unexpected ports, query strings, and fragments are rejected. 7. Rotate any API key, user hash, or password that may already have been sent to an untrusted endpoint.
Vulnerability Patterns
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
Findings (8)

Tainted flow: 'request' from os.getenv (line 54, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Content
def _request(self, method: str, url: str, body: bytes | None = None, headers: dict[str, str] | None = None) -> Any:
        request = urllib.request.Request(url, data=body, headers=headers or {"Accept": "application/json"}, method=method)
        try:
            with urllib.request.urlopen(request, timeout=self.timeout) as response:  # noqa: S310 - URL is API base/user input by design.
                raw = response.read().decode("utf-8")
        except urllib.error.HTTPError as exc:
            detail = exc.read().decode("utf-8", errors="replace")
Confidence
92% confidence
Finding
The CLI allows the request destination to be overridden via --base-url or BRICKSET_BASE_URL, and then sends authenticated requests containing the API key and, for private operations, the Brickset user hash to that URL. In an agent-skill context, attacker-influenced command arguments or environment could redirect requests to an arbitrary host, causing credential exfiltration and potential SSRF to internal services.

Lp3

Medium
Category
MCP Least Privilege
Confidence
88% confidence
Finding
The skill clearly instructs use of environment variables containing sensitive credentials and a CLI that performs networked API calls, but it declares no explicit tool scope or permission boundaries. Without allowed-tools/permissions metadata, a hosting agent may grant broader-than-intended access to env and network capabilities, increasing the chance of unintended secret exposure or unauthorized external requests.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
The OpenAPI spec exposes a login endpoint that accepts username and password and a setCollection endpoint that can modify a user's collection, wishlist, notes, and ratings, but it does not include any user-facing warning, confirmation, or explicit consent semantics around credential submission or account-changing actions. In an agent skill context, this is dangerous because the model or orchestrator may invoke these actions with insufficient user awareness, leading to unintended account access or silent modification of personal data.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
The prompt states that collection and wishlist operations are 'instant' and immediately modify the user's Brickset profile, but it does not require warning or confirming with the user before persistent account changes. That increases the risk of unintended profile edits from ambiguous requests or mistaken tool selection.

Vague Triggers

Medium
Confidence
85% confidence
Finding
The prompt uses broad phrases like 'add to my collection' and 'show my collection' as activation examples for account-affecting behavior. In a multi-skill environment, this can cause the skill to claim common user intents too aggressively and perform or prepare state-changing actions when the user did not clearly intend Brickset specifically.

Autonomous Decision Making

Medium
Category
Excessive Agency
Content
- Always confirm which service(s) were updated in your response

3. If ONLY ONE service is configured:
   - Use that service without asking
   - Still mention which service was used (e.g., "Added to your Brickset collection")

4. When user explicitly specifies the service (e.g., "add to Brickset"):
Confidence
88% confidence
Finding
The instruction to 'use that service without asking' when only one service is configured permits autonomous execution of account-affecting actions without an explicit confirmation boundary. Even if service selection is unambiguous, the underlying user request may still be ambiguous, making silent writes to a persistent collection risky.

Autonomous Decision Making

Medium
Category
Excessive Agency
Content
You: "I can show your Rebrickable set lists or your Brickset collection. Which would you like to see?"

- User: "Add 10179 to my Brickset collection"
  You: [Call addToBricksetCollection directly without asking]
Confidence
75% confidence
Finding
Skill enables autonomous high-impact decisions without human-in-the-loop verification. Critical operations (destructive commands, financial transactions, data deletion) should require explicit user confirmation.

Dynamic attribute access via getattr()

Low
Category
Dangerous Code Execution
Content
def require_any_change(args: argparse.Namespace, field_names: Iterable[str]) -> None:
    if getattr(args, "params_json", None):
        return
    if not any(getattr(args, field) is not None for field in field_names):
        raise BricksetCliError("provide at least one collection/wishlist field to change")
Confidence
50% confidence
Finding
Dynamic getattr() with a non-literal attribute name can access arbitrary object attributes, potentially bypassing access controls.

Static analysis

No suspicious patterns detected.