Back to skill

Security audit

Idea Capture

Security checks for vulnerabilities and agentic risk

Overview

This idea-saving skill does what it says, but an unconstrained idea ID can make it write files outside its intended ideas folder.

Review or fix the script before installing in a workspace that may process untrusted titles, idea IDs, or catalog data. At minimum, restrict idea IDs to a slug format, validate IDs loaded from catalog.json, and enforce that all resolved write paths stay under the intended ideas directory.

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/idea_capture.py:169
Finding

User-Controlled Idea ID Enables Path Traversal and Arbitrary File Writes

Content
View full analysis

Vulnerability Details

File Location: scripts/idea_capture.py:103-105, scripts/idea_capture.py:169-170, and scripts/idea_capture.py:193-194
Vulnerability Type: Path traversal and unrestricted filesystem write
Risk Level: High

The --idea-id argument is used as a filesystem path component without validation, normalization, or a check that the resulting path remains inside the intended ideas directory.

Vulnerable Code

python
def write_summary(idea_id: str, title: str, source: str, summary: str, notes: str, open_questions, next_steps, stamp: str):
    target_dir = SUMMARIES_DIR / idea_id
    target_dir.mkdir(parents=True, exist_ok=True)
    path = target_dir / f"{stamp}.md"
python
if idea is None:
    idea_id = args.idea_id or slugify(args.title)
    idea_path = IDEAS_DIR / f"{idea_id}.md"
    meta = {
        "id": idea_id,
        "title": args.title,
        "status": "active",
        "tags": tags,
        "created": today,
        "updated": updated_at,
        "source": args.source,
    }
    idea_path.write_text(
        render_idea_doc(meta, args.summary, args.notes, args.open_question, args.next_step, update_entry),
        encoding="utf-8",
    )
python
else:
    idea_id = idea["id"]
    idea_path = IDEAS_DIR / f"{idea_id}.md"
    merged_tags = sorted(set(idea.get("tags", [])) | set(tags))

Technical Analysis

Values supplied through --idea-id bypass slugify() because the explicit value is selected directly:

python
idea_id = args.idea_id or slugify(args.title)

Consequently, an ID can contain parent-directory components such as ../ or be an absolute path. The value is subsequently joined to IDEAS_DIR and SUMMARIES_DIR using pathlib.Path. These joins do not guarantee containment within the intended archive. Parent-directory components are resolved by the operating system during file access, while an ...[truncated 2843 chars]

Remediation
View remediation

Remediation Suggestions

  1. Apply strict identifier validation

    Permit only a conservative identifier format and reject all other values:

    python
    IDEA_ID_RE = re.compile(r"^[a-z0-9][a-z0-9-]{0,127}$")
    
    def validate_idea_id(value: str) -> str:
        if not IDEA_ID_RE.fullmatch(value):
            raise ValueError(
                "Invalid idea ID: use lowercase letters, digits, and hyphens only."
            )
        return value
    

    Apply this validation to explicit CLI identifiers and IDs loaded from catalog.json.

  2. Enforce canonical path containment

    Resolve the base directory and destination before every read, write, or directory-creation operation:

    python
    def contained_path(base: Path, relative: str) -> Path:
        base = base.resolve()
        destination = (base / relative).resolve()
        if not destination.is_relative_to(base):
            raise ValueError("Path escapes the intended storage directory.")
        return destination
    

    For example:

    python
    idea_id = validate_idea_id(args.idea_id) if args.idea_id else slugify(args.title)
    idea_path = contained_path(IDEAS_DIR, f"{idea_id}.md")
    target_dir = contained_path(SUMMARIES_DIR, idea_id)
    
  3. Validate persisted catalog records

    Treat catalog.json as untrusted input. Verify its schema and validate every stored ID before using it in a path. Reject or quarantine malformed entries rather than attempting to update them.

  4. Prevent unintended overwrites in create mode

    Use exclusive creation when a new record is expected:

    python
    with idea_path.open("x", encoding="utf-8") as handle:
        handle.write(content)
    

    Alternatively, explicitly detect an existing destination and require confirmation before replacement.

  5. Add security regression tests

    Test and reject identifiers containing ../, .., path separators, absol ...[truncated 200 chars]

Vulnerability Patterns
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
Findings (1)

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
93% confidence
Finding

The skill instructs the agent to run a local script that performs file writes and updates multiple repository paths, but the manifest does not declare any tool scope such as permissions or allowed-tools. This creates an authorization ambiguity where an agent may execute file read/write operations without explicit least-privilege constraints, increasing the chance of unintended or overbroad filesystem modification.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.