Back to skill

Security audit

Personal Finance Reconciler

Security checks for vulnerabilities and agentic risk

Overview

The skill is a coherent local finance tracker, but its HTML report generation can turn imported transaction text into active browser content, so users should review it before installing.

Install only if you are comfortable with a local tool storing your bank transactions in SQLite. Use a virtual environment, import statements only from trusted sources, avoid opening HTML reports generated from untrusted or modified files, and prefer text/JSON reports until the HTML escaping issue is fixed.

Vulnerability Patterns
  • Insecure DependenciesIntroduces malicious components through unsafe dependency sources
  • 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
Findings (3)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/report.py:365
Finding
Stored HTML and Script Injection in Generated Financial Reports<![CDATA[ ## Vulnerability Details **File Location**: `scripts/report.py:365-378` **Vulnerability Type**: Stored HTML injection / stored cross-site scripting **Risk Level**: High ### Vulnerable Code ```python # Top merchants if report["top_merchants"]: html += " <h2>Top Merchants</h2>\n <table>\n" html += " <tr><th>Merchant</th><th>Amount</th><th>Transactions</th></tr>\n" for m in report["top_merchants"]: html += f" <tr><td>{m['merchant']}</td><td>{m['total_formatted']}</td>" html += f"<td>{m['count']}</td></tr>\n" html += " </table>\n" # Largest transactions if report["largest_transactions"]: html += " <h2>Largest Transactions</h2>\n <table>\n" html += " <tr><th>Date</th><th>Description</th><th>Amount</th><th>Category</th></tr>\n" for l in report["largest_transactions"]: html += f" <tr><td>{l['date']}</td><td>{l['description']}</td>" html += f"<td>{l['amount_formatted']}</td><td>{l['category'] or ''}</td></tr>\n" html += " </table>\n" ``` ### Technical Analysis Merchant names and transaction descriptions originate from imported CSV or OFX/QFX statements. These values are persisted in SQLite and later interpolated directly into HTML without HTML entity escaping. Because characters such as `<`, `>`, `"`, `'`, and `&` are not escaped, a crafted transaction description can break out of the intended table cell and introduce arbitrary HTML or JavaScript-capable elements. For example, a description containing an image element with an event handler would be emitted as active markup rather than displayed as text. This is a stored injection vulnerability: the malicious value is first saved in the transaction database and can execute later whenever an HTML report containing that transaction is generated and opened. ### Attack Path 1. An attacker creates or modifies a CSV, OFX, or QFX statement containing a transaction description with malicious HTML. 2. T ...[truncated 1221 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Escape every dynamic value before placing it in HTML: ```python from html import escape merchant = escape(str(m["merchant"]), quote=True) description = escape(str(l["description"]), quote=True) category = escape(str(l["category"] or ""), quote=True) ``` 2. Prefer a template engine with automatic escaping enabled instead of constructing HTML through string concatenation. 3. Apply escaping according to output context. HTML text, attributes, URLs, CSS, and JavaScript require different encoding rules. 4. Add a restrictive Content Security Policy, such as disallowing inline scripts and restricting network destinations. 5. Add regression tests using descriptions containing HTML metacharacters, event handlers, script elements, encoded payloads, and malformed tags. 6. Treat statement contents as untrusted even when files appear to originate from a supported bank. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/categorize.py:47
Finding
User-Controlled Regular Expressions Permit Denial of Service<![CDATA[ ## Vulnerability Details **File Location**: `scripts/categorize.py:47-52` and `scripts/categorize.py:149-160` **Vulnerability Type**: Regular-expression denial of service **Risk Level**: Medium ### Vulnerable Code The categorization engine evaluates persisted patterns using Python's backtracking regular-expression engine: ```python # 4. Regex pattern match for rule in rules_by_type.get("regex", []): try: if re.search(rule["pattern"], description, re.IGNORECASE): return rule["category_name"], 0.75 except re.error: continue ``` The rule-management function accepts and persists arbitrary regex patterns without complexity validation: ```python if rule_type not in ("exact", "keyword", "regex", "custom"): conn.close() return {"success": False, "error": f"Invalid rule type: {rule_type}"} # Custom rules get highest priority priority = 100 if rule_type == "custom" else 50 conn.execute( "INSERT INTO categorization_rules (category_id, rule_type, pattern, priority) VALUES (?, ?, ?, ?)", (cat_id, rule_type, pattern, priority), ) ``` ### Technical Analysis Python's standard `re` engine uses backtracking. Certain patterns containing nested or ambiguous quantifiers can require exponential processing time for non-matching inputs. The `add-rule` command permits an arbitrary pattern to be stored with `--type regex`. During later categorization, that pattern is evaluated against every applicable transaction description without: - An execution timeout. - Pattern-complexity validation. - A pattern-length limit. - A transaction-description length limit. - Isolation in a cancellable worker process. Catching `re.error` only handles syntactically invalid expressions. It does not prevent valid expressions with catastrophic backtracking. ### Attack Path 1. A malicious or improperly instructed user adds a pathological categorization rule using `categorize.py add-rule ... --type regex`. 2. The pattern is persisted in ...[truncated 980 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Prefer keyword, exact, or glob-style matching when full regular expressions are unnecessary. 2. Use a regular-expression implementation that guarantees linear-time matching or supports reliable execution timeouts. 3. Reject patterns containing dangerous nested quantifiers, ambiguous alternation, excessive repetition, or backreferences. Validation should be treated as defense in depth rather than a complete solution. 4. Enforce strict maximum lengths for both stored patterns and transaction descriptions. 5. If a timeout-capable engine cannot be used, execute regex evaluation in an isolated worker process that can be terminated after a short deadline. 6. Compile and validate a regex before persisting it. 7. Provide a rule deletion or disabling mechanism so a problematic persisted rule can be safely recovered. 8. Add automated tests using known catastrophic-backtracking patterns and long non-matching descriptions. ]]>

T08 · Insecure Dependencies

Warning
Location
SKILL.md:5
Finding
Unpinned Third-Party Dependencies Create Supply-Chain Drift<![CDATA[ ## Vulnerability Details **File Location**: `SKILL.md:5`, `SKILL.md:17`, and `skill.json:14-17` **Vulnerability Type**: Unpinned package installation **Risk Level**: Medium ### Vulnerable Code `SKILL.md` declares and instructs installation without version or integrity constraints: ```yaml install: pip3 install pandas ofxparse tabulate python-dateutil ``` ```bash pip3 install pandas ofxparse tabulate python-dateutil ``` The metadata repeats the same mutable installation commands: ```json "install": { "all": "pip install pandas ofxparse tabulate python-dateutil", "macos": "pip3 install pandas ofxparse tabulate python-dateutil", "linux": "pip3 install pandas ofxparse tabulate python-dateutil", "windows": "pip install pandas ofxparse tabulate python-dateutil" } ``` ### Technical Analysis The installation commands resolve whichever package versions are current at installation time. No exact versions, lock file, package hashes, or controlled index configuration are supplied. As a result, the code installed and imported by the Skill can differ from the dependency versions that were present during review. These packages process sensitive financial files and execute Python code in the user's environment, so a compromised, malicious, or unexpectedly incompatible future release could materially change the Skill's behavior. No evidence was found that the named packages are currently malicious or typosquatted. The confirmed weakness is the absence of reproducible, integrity-checked dependency resolution. ### Attack Path 1. A future dependency release is compromised, malicious, or otherwise unsafe, or the resolver obtains an unintended version from its configured package source. 2. A user follows the required first-time installation command. 3. `pip` resolves and downloads the mutable package version without checking a project-provided hash. 4. Package installation hooks or subsequently imported package code execute in the user's Python environme ...[truncated 772 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Pin every direct dependency to a reviewed exact version. 2. Generate a lock or requirements file containing cryptographic hashes, for example through a reproducible dependency-management workflow. 3. Install with hash verification enabled, such as `pip install --require-hashes -r requirements.txt`. 4. Pin and review transitive dependencies as well as direct dependencies. 5. Explicitly configure and document the trusted package index rather than relying on ambient `pip` configuration. 6. Run dependency vulnerability and provenance checks in continuous integration. 7. Update dependencies through a controlled review process, regenerating hashes only after testing and security review. 8. Recommend installation inside an isolated virtual environment rather than the user's global Python environment. ]]>
Vulnerability Patterns
  • System Prompt LeakageDirect Leakage, Indirect Extraction, Tool-Based Exfiltration
  • 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
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
Findings (14)

Ae1

High
Category
analysis-evasion
Content
python3 scripts/import_csv.py <file_path> [--format chase|bofa|wells_fargo|generic] [--account <name>]
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
python3 scripts/import_csv.py <file_path> [--format chase|bofa|wells_fargo|generic] [--account <name>]
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Direct Prompt Extraction

High
Category
System Prompt Leakage
Content
# 1. Custom rules (highest priority)
    for rule in rules_by_type.get("custom", []):
        if rule["pattern"].lower() == desc_lower:
            return rule["category_name"], 1.0

    # 2. Exact keyword match
    for rule in rules_by_type.get("keyword", []):
Confidence
85% confidence
Finding
Skill contains instructions that could directly expose system prompts, internal rules, or hidden instructions to users or external parties.

Direct Prompt Extraction

High
Category
System Prompt Leakage
Content
# 1. Custom rules (highest priority)
    for rule in rules_by_type.get("custom", []):
        if rule["pattern"].lower() == desc_lower:
            return rule["category_name"], 1.0

    # 2. Exact keyword match
    for rule in rules_by_type.get("keyword", []):
Confidence
85% confidence
Finding
Skill contains instructions that could directly expose system prompts, internal rules, or hidden instructions to users or external parties.

Direct Prompt Extraction

High
Category
System Prompt Leakage
Content
# 1. Custom rules (highest priority)
    for rule in rules_by_type.get("custom", []):
        if rule["pattern"].lower() == desc_lower:
            return rule["category_name"], 1.0

    # 2. Exact keyword match
    for rule in rules_by_type.get("keyword", []):
Confidence
85% confidence
Finding
Skill contains instructions that could directly expose system prompts, internal rules, or hidden instructions to users or external parties.

Direct Prompt Extraction

High
Category
System Prompt Leakage
Content
rules_by_type[rt] = []
        rules_by_type[rt].append(dict(rule))
    conn.close()
    return rules_by_type


class TestCategorizeTransaction:
Confidence
85% confidence
Finding
Skill contains instructions that could directly expose system prompts, internal rules, or hidden instructions to users or external parties.

Lp3

Medium
Category
MCP Least Privilege
Confidence
85% confidence
Finding
The skill instructs the agent to run local commands, install packages, initialize a database, and use an environment variable override, but it does not declare any explicit tool scope or permissions. That mismatch can cause the agent to invoke broader local execution or environment access than a user would reasonably expect, increasing the risk of unauthorized command execution or unintended exposure of local filesystem and environment data.

Vague Triggers

Medium
Confidence
97% confidence
Finding
The onboarding trigger includes broad phrases like 'get started' and 'what can you do', which can match benign general conversation and cause the skill to pivot into finance-specific workflow unexpectedly. In this skill, that can lead the agent to solicit sensitive financial documents or file paths when the user may only be asking for generic help, increasing the chance of inappropriate data collection or user confusion.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
The delete_budget function executes a DELETE statement and commits the change, but there is no confirmation prompt or user-facing warning before performing this destructive operation. Although the docstring names the action, the code does not disclose the irreversible effect at execution time.

Missing User Warnings

Medium
Confidence
84% confidence
Finding
This code updates category assignments for every selected transaction and commits the changes, which is a file-backed data modification with potentially broad impact. Although the script purpose is categorization, there is no visible prompt, print/log disclosure, or inline warning around the bulk write operation to inform users that existing transaction records will be modified, especially when recategorization is enabled.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
This CLI prints the full query result JSON directly to stdout, and list/max/min queries can include sensitive transaction details such as dates, descriptions, merchants, categories, and amounts. In a personal-finance skill, stdout may be captured by shell history tooling, terminal logs, parent processes, CI runners, or other local observability mechanisms, creating unintended disclosure of private financial data.

Missing User Warnings

Medium
Confidence
85% confidence
Finding
This code generates a full HTML report containing transaction descriptions, spending totals, top merchants, and budget data, which are sensitive personal financial details. The file includes no confirmation prompt, warning message, or explicit disclosure that exporting in HTML may create a persistent, shareable artifact containing private data.

Natural-Language Policy Violations

Low
Confidence
91% confidence
Finding
The generated HTML explicitly sets `lang="en"`, forcing English as the document language. There is no user choice or documented justification for this locale restriction, which conflicts with the policy against imposing a specific language without opt-in.

Natural-Language Policy Violations

Low
Confidence
79% confidence
Finding
The CSV contains natural-language content such as U.S.-specific place names and transaction descriptors like "DIRECT DEPOSIT" and merchant locations (e.g. OAKLAND CA, SAN FRAN CA, OAK PARK IL). Because SQP-3 applies to all file types and covers locale/language policy, this file hard-codes a specific locale context without any opt-in or justification visible in the file.

Static analysis

No suspicious patterns detected.