Back to skill

Security audit

Recipe to List

Security checks for vulnerabilities and agentic risk

Overview

The skill’s recipe-to-shopping-list purpose is coherent, but it uses broad local credential loading and live Todoist/workspace changes that need review before installation.

Review and harden this skill before installing. Use --dry-run and --no-save first, avoid the shell wrapper or replace its dotenv sourcing with an allowlisted parser for only GEMINI_API_KEY/GOOGLE_API_KEY and TODOIST_API_TOKEN, and require a confirmation step before Todoist updates. Install only if you are comfortable sending recipe photos or extracted text to Gemini and writing recipe files into the workspace.

Vulnerability Patterns
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
  • 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
  • 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 (3)

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/recipe-to-list.sh:10
Finding
Executable dotenv loading permits arbitrary shell execution and overexposes secrets<![CDATA[ ## Vulnerability Details **File Location**: `scripts/recipe-to-list.sh`, lines 10-13 **Vulnerability Type**: Unsafe configuration-file execution and excessive credential propagation **Risk Level**: Medium ### Vulnerable Code ```bash # Load keys/tokens set -a [[ -f ~/.clawdbot/.env ]] && source ~/.clawdbot/.env set +a ``` ### Technical Analysis The wrapper uses the Bash `source` command to load `~/.clawdbot/.env`. This does not parse the file as passive key/value configuration: it executes every statement in the file as shell code under the current user's privileges. The surrounding `set -a` also automatically exports every variable defined by the file. Consequently, unrelated credentials or sensitive configuration values are inherited by the Python process and its `todoist` subprocesses, even though the declared functionality only requires `GEMINI_API_KEY` or `GOOGLE_API_KEY` and `TODOIST_API_TOKEN`. The credential access is broader than the minimum privilege needed by the Skill. The wrapper neither restricts loaded variable names nor validates the file's ownership, permissions, format, or contents. ### Attack Path 1. An attacker, compromised process, malicious installer, or other component obtains write access to `~/.clawdbot/.env`. 2. The attacker inserts a shell statement or command substitution into the file rather than a normal environment assignment. 3. The user invokes `recipe-to-list.sh`. 4. Bash evaluates the entire file through `source`. 5. The injected commands execute with the invoking user's permissions. 6. All values loaded while `set -a` is active are also exposed to the Python process and subsequently launched child processes. This path requires the attacker to be able to create or modify the referenced dotenv file, but the wrapper converts that otherwise passive configuration access into a direct execution primitive. ### Impact Assessment Successful exploitation permits arbitrary command execution with the privileges of th ...[truncated 413 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Do not use `source`, `.`, `eval`, or command substitution to parse dotenv files. - Prefer requiring the caller to provide the three documented environment variables directly. - If dotenv support is necessary, use a strict parser that accepts only allowlisted key/value assignments: - `GEMINI_API_KEY` - `GOOGLE_API_KEY` - `TODOIST_API_TOKEN` - Reject shell operators, substitutions, functions, redirections, and additional variable names. - Validate that the dotenv file is owned by the current user and is not writable by group or other users. - Avoid `set -a`; construct an explicit, minimal environment for Python and child processes. - In Python, invoke Todoist with an allowlisted environment rather than inheriting the full parent environment. - Document the credential-loading behavior and provide a mode that does not read any home-directory credential file. ]]>

T02 · Agent Memory Poisoning

Warning
Location
scripts/recipe_to_list.py:798
Finding
Untrusted recipe and model content is persisted into an agent-facing knowledge base<![CDATA[ ## Vulnerability Details **File Location**: `scripts/recipe_to_list.py`, lines 798-825; related data flow at lines 1028-1064 and 1134-1142 **Vulnerability Type**: Persistent untrusted-content injection **Risk Level**: Medium ### Vulnerable Code ```python path = out_dir / f"{d}--{slug}.md" # avoid overwrite if path.exists(): path = out_dir / f"{d}--{slug}-{os.getpid()}.md" ing = "\n".join([f"- {x}" for x in items]) md = ( f"# {t}\n\n" f"- Date cooked: {d}\n" f"- Source: {source}\n\n" f"## Ingredients\n\n{ing}\n\n" ) if notes.strip(): md += f"## Notes\n\n{notes.strip()}\n" path.write_text(md, encoding="utf-8") # update index idx = out_dir / "index.md" if not idx.exists(): idx.write_text( "# Cookbook Index\n\n| Date cooked | Recipe | Tags | Rating | Source |\n|---|---|---|---:|---|\n", encoding="utf-8", ) rel = path.as_posix() row = f"| {d} | [{t}]({rel}) | | | {source} |\n" with idx.open("a", encoding="utf-8") as f: f.write(row) ``` The persisted values originate from model output and command-line input: ```python extracted = gemini_extract_items(args.image, args.model, api_key, timeout=args.timeout) ... source = (args.source or f"photo:{args.image}").strip() ... saved_path = save_recipe_to_workspace( title=title, source=source, items=add_list, notes=notes, recipes_dir=str(repo_root / "recipes"), ) ``` ### Technical Analysis The recipe title, ingredient list, notes, and source are derived from Gemini output, image content, or caller-controlled command-line arguments. These values are interpolated directly into Markdown files and the cookbook index without Markdown escaping, active-content filtering, trust labeling, or confirmation. The Skill documentation describes `recipes/` as a cookbook knowledge base. If an AI agent later reads this directory as trusted workspace context, instruction-like text embedded in a recipe image, source value, title, notes, or ingredient ca ...[truncated 1895 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Treat all image-derived, model-generated, and caller-supplied strings as untrusted data. - Validate the returned object against a strict schema, including: - Expected field types - Maximum title, note, source, and ingredient lengths - Maximum ingredient count - Rejection of embedded control characters and multiline values where unnecessary - Escape Markdown metacharacters before persistence, especially newlines, table delimiters, brackets, parentheses, and HTML constructs. - Store source values in an encoded or structured metadata format rather than interpolating them into a Markdown table. - Clearly delimit generated content as quoted, untrusted data and instruct downstream consumers never to treat cookbook text as operational instructions. - Require explicit user confirmation before saving model-produced notes or other free-form content. - Consider storing recipes as JSON or another structured data format and generating Markdown only for display. - Keep generated recipe data outside directories automatically ingested as trusted agent instructions or long-term behavioral memory. - Add tests using titles, sources, notes, and ingredients containing newlines, Markdown links, HTML, table delimiters, and instruction-like text. ]]>

T09 · Insecure Skill Coding Practices

Note
Location
scripts/recipe_to_list.py:940
Finding
Undefined section-handling symbols cause crashes after successful Todoist mutations<![CDATA[ ## Vulnerability Details **File Location**: `scripts/recipe_to_list.py`, lines 940-996 **Vulnerability Type**: Non-atomic remote mutation and post-operation runtime failure **Risk Level**: Low ### Vulnerable Code After updating a task whose existing entry had no quantity: ```python if tid: ids.append(tid) if target_section: _move_task(str(ex_id), project, target_section) continue ``` After updating and summing compatible quantities: ```python if tid: ids.append(tid) if target_section: _move_task(str(ex_id), project, target_section) continue ``` After adding a new task: ```python if tid: ids.append(tid) if target_section: _move_task(str(tid), project, target_section) ``` No definition of `target_section` or `_move_task` exists in the audited project. ### Technical Analysis Each affected branch performs a Todoist mutation before evaluating `target_section`. When Todoist successfully returns a task ID, Python reaches the undefined variable and raises `NameError`. This creates a non-atomic execution sequence: the externally visible action succeeds, but local execution fails before the script can save the cookbook entry or emit its final JSON report. The defect also conflicts with the documented behavior that the Shopping list is kept flat and does not use sections. Because callers may interpret the nonzero exit as evidence that no change occurred, automated or manual retries can produce repeated processing, inconsistent quantities, or confusing partial state. ### Attack Path 1. The user runs the Skill without `--dry-run`. 2. The script successfully authenticates to Todoist. 3. A task is added or an existing task is updated. 4. Todoist returns a valid task ID, confirming that the remote mutation has already occurred. 5. The script evaluates the undefined `target_section` name. 6. Python raises `NameError` and terminates before completing subsequent local operations and final reporting. 7. The user or an automa ...[truncated 735 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Remove all `target_section` and `_move_task` references because the declared design uses a flat Todoist list. - If section support is intended, explicitly define and initialize `target_section`, implement `_move_task`, and add it to the documented interface. - Validate all required state before making any remote mutation. - Add automated tests for: - Adding a new task - Updating a task with a newly supplied quantity - Summing compatible quantities - Skipping overlapping tasks - Dry-run behavior - Return explicit information about mutations that completed before any later failure. - Where the Todoist API permits it, introduce compensating actions or idempotency controls so retries do not repeat successful mutations. - Ensure the command's exit status and final report accurately reflect whether remote changes occurred. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
Findings (20)

Tainted flow: 'req' from os.environ.get (line 528, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Content
method="POST",
        )
        try:
            with urllib.request.urlopen(req, timeout=timeout) as resp:
                raw = resp.read().decode("utf-8")
            break
        except urllib.error.HTTPError as e:
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tainted flow: 'req' from os.environ.get (line 528, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Content
method="POST",
        )
        try:
            with urllib.request.urlopen(req, timeout=timeout) as resp:
                raw = resp.read().decode("utf-8")
            break
        except urllib.error.HTTPError as e:
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding
The documented behavior does not accurately match the implemented or required capabilities: it claims recipe web search/fetch flow and benign recipe processing, but the skill also depends on undeclared external network calls, Todoist CLI execution, and workspace writes. Description-behavior mismatch is dangerous because users and security controls may approve the skill under incomplete assumptions, leading to unanticipated data exfiltration to Gemini or unintended modification of Todoist and local files.

Credential Access

High
Category
Privilege Escalation
Content
# Load keys/tokens
set -a
[[ -f ~/.clawdbot/.env ]] && source ~/.clawdbot/.env
set +a

python3 "$(dirname "$0")/recipe_to_list.py" --image "$IMG" --project "Shopping" --source "photo:$IMG"
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Lp3

Medium
Category
MCP Least Privilege
Confidence
96% confidence
Finding
The skill advertises operational behavior that requires sensitive capabilities (environment access, file write, network access, shell/CLI invocation) but does not declare any tool scope or permissions. This weakens reviewability and consent, making it easier for a user or orchestrator to invoke a skill that can modify Todoist data, write workspace files, and send content to external services without explicit capability boundaries.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
The skill sends recipe photos or fetched page text to Gemini for ingredient extraction, but the description does not clearly disclose this external transmission. This is a privacy and compliance risk because images or recipe pages may contain personal notes, metadata, subscription content, or other sensitive information that users may not expect to leave their environment.

Missing User Warnings

Medium
Confidence
88% confidence
Finding
The skill text describes updating the Shopping list but does not prominently warn that it will create or modify Todoist tasks, which is a state-changing action in a third-party account. In agent settings, insufficient disclosure around write operations can cause accidental data modification, duplicate tasks, or unwanted automation if a user expects analysis only.

Context-Inappropriate Capability

Medium
Confidence
91% confidence
Finding
The wrapper unconditionally sources ~/.clawdbot/.env and exports every variable into the child process environment. Even if intended to provide API keys, this grants the skill access to all secrets stored in that file, which exceeds the minimal privileges implied by a recipe-to-shopping-list tool and increases the blast radius if the downstream Python script, its dependencies, or any network call mishandles environment data.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
The script silently loads local secrets from ~/.clawdbot/.env without informing the user at runtime. In this skill's context, which processes user-supplied recipe images and may call external services, undisclosed secret loading makes the data flow less transparent and can expose unrelated credentials to components the user would not expect.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
def _get_existing_project_tasks(project: str) -> list[dict]:
    cp = subprocess.run(
        ["todoist", "tasks", "--all", "-p", project, "--json"],
        capture_output=True,
        text=True,
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
# If existing has no qty but new does, set existing to new qty.
                if (ex_qty is None or not ex_unit) and (new_qty is not None and new_unit):
                    new_content = _rewrite_with_total(ex_content, new_qty, new_unit)
                    cp = subprocess.run(
                        ["todoist", "update", str(ex_id), "--content", new_content, "--json"],
                        capture_output=True,
                        text=True,
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
# If existing has no qty but new does, set existing to new qty.
                if (ex_qty is None or not ex_unit) and (new_qty is not None and new_unit):
                    new_content = _rewrite_with_total(ex_content, new_qty, new_unit)
                    cp = subprocess.run(
                        ["todoist", "update", str(ex_id), "--content", new_content, "--json"],
                        capture_output=True,
                        text=True,
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Tainted flow: 'new_content' from os.environ.get (line 928, credential/environment) → subprocess.run (code execution)

Medium
Category
Data Flow
Content
# If existing has no qty but new does, set existing to new qty.
                if (ex_qty is None or not ex_unit) and (new_qty is not None and new_unit):
                    new_content = _rewrite_with_total(ex_content, new_qty, new_unit)
                    cp = subprocess.run(
                        ["todoist", "update", str(ex_id), "--content", new_content, "--json"],
                        capture_output=True,
                        text=True,
Confidence
65% confidence
Finding
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Tainted flow: 'new_content' from os.environ.get (line 958, credential/environment) → subprocess.run (code execution)

Medium
Category
Data Flow
Content
total = None

                if 'total' in locals() and total is not None:
                    cp = subprocess.run(
                        ["todoist", "update", str(ex_id), "--content", new_content, "--json"],
                        capture_output=True,
                        text=True,
Confidence
65% confidence
Finding
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
if existing_task and skip_overlap:
            continue

        cp = subprocess.run(
            ["todoist", "add", title, "--project", project, "--json"],
            capture_output=True,
            text=True,
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Tainted flow: 'title' from os.environ.get (line 1044, credential/environment) → subprocess.run (code execution)

Medium
Category
Data Flow
Content
if existing_task and skip_overlap:
            continue

        cp = subprocess.run(
            ["todoist", "add", title, "--project", project, "--json"],
            capture_output=True,
            text=True,
Confidence
65% confidence
Finding
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Description-Behavior Mismatch

Medium
Confidence
98% confidence
Finding
The manifest says the skill can extract ingredients from either recipe photos or recipe web pages via search and fetch. In this file, the CLI requires an `--image` argument, verifies that image path exists, and then calls only the image-based Gemini extraction flow; there is no webpage search, fetch, or URL parsing behavior implemented here.

Missing User Warnings

Medium
Confidence
96% confidence
Finding
The script automatically uploads the provided recipe photo to Gemini without an explicit runtime warning or confirmation. Because photos may contain personal notes, kitchen surroundings, or other incidental sensitive information, this creates a privacy risk through third-party data disclosure in a consumer productivity skill.

Intent-Code Divergence

Medium
Confidence
95% confidence
Finding
The code comments say the list stays flat, but the function contains live references to target_section and _move_task even though neither is defined in this file. In non-dry-run operation this can raise a NameError after creating or updating tasks, causing partial side effects and unreliable behavior that can corrupt workflow integrity.

Missing User Warnings

Medium
Confidence
88% confidence
Finding
The skill mutates the user's Todoist shopping list and can update existing tasks based on model-derived parsing without an explicit confirmation step. In this context, integrity matters: a mistaken extraction or hallucinated quantity can silently alter shopping data and create unintended purchases or loss of existing list accuracy.

Static analysis

No suspicious patterns detected.