Back to skill

Security audit

Expense Tracker

Security checks for vulnerabilities and agentic risk

Overview

The skill is a coherent local expense tracker, but it needs Review because malformed date or month inputs can make it read, create, copy, or modify Markdown files outside its intended expenses folder.

Install only if you are comfortable with a local expense tracker that writes under the configured workspace and automatically backs up existing monthly ledgers to ~/Documents/expenses_backup. Treat it as needing review or fixes before routine use: validate dates and months, keep generated paths inside the expenses directory, and escape Markdown table fields to prevent malformed inputs from corrupting or touching unintended Markdown files.

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 (2)

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/log_expense.py:13
Finding

Path Traversal Through Unvalidated Month and Date Parameters

Content
View full analysis

Vulnerability Details

File Location: scripts/log_expense.py:13-21, with unsafe input propagated at scripts/log_expense.py:42-49 and scripts/log_expense.py:131-132
Vulnerability Type: Path traversal and insufficient input validation
Risk Level: Medium

Vulnerable Code

python
def get_expense_file(year_month: str, workspace_path: str = None) -> Path:
    """Get the path to the expense file for a given month."""
    if workspace_path is None:
        workspace_path = Path.home() / ".openclaw" / "workspace"
    else:
        workspace_path = Path(workspace_path)
    
    expenses_dir = workspace_path / "expenses"
    expenses_dir.mkdir(exist_ok=True)
    
    return expenses_dir / f"{year_month}.md"

Unsafe values reach this function through both supported commands:

python
def log_expense(amount: float, category: str, description: str = "", tags: str = "",
                date: str = None, workspace_path: str = None):
    """Log an expense entry."""
    if date is None:
        date = datetime.now().strftime("%Y-%m-%d")
    
    year_month = date[:7]  # YYYY-MM
    file_path = get_expense_file(year_month, workspace_path)
python
elif args.command == "summary":
    year_month = args.year_month or datetime.now().strftime("%Y-%m")
    summary = get_monthly_summary(year_month, args.workspace)

Technical Analysis

The application documents YYYY-MM and YYYY-MM-DD formats but does not enforce them. The year_month value is concatenated into a filesystem path without rejecting path separators or .. components. pathlib therefore permits the resulting path to resolve outside the intended expenses directory.

The summary command passes its unrestricted positional value directly into get_expense_file(). The log command derives the filename from the first seven characters of the supplied date, but those characters can still contain a travers ...[truncated 2065 chars]

Remediation
View remediation

Remediation Suggestions

  1. Strictly validate month values before using them in paths:

    python
    import re
    
    YEAR_MONTH_RE = re.compile(r"^\d{4}-(0[1-9]|1[0-2])$")
    
    def validate_year_month(value: str) -> str:
        if not YEAR_MONTH_RE.fullmatch(value):
            raise ValueError("Month must use YYYY-MM format")
        return value
    
  2. Parse dates rather than slicing strings:

    python
    parsed_date = datetime.strptime(date, "%Y-%m-%d")
    year_month = parsed_date.strftime("%Y-%m")
    date = parsed_date.strftime("%Y-%m-%d")
    
  3. Resolve the expense directory and candidate path, then verify containment:

    python
    expenses_dir = (workspace_path / "expenses").resolve()
    expenses_dir.mkdir(parents=True, exist_ok=True)
    
    candidate = (expenses_dir / f"{year_month}.md").resolve()
    if candidate.parent != expenses_dir:
        raise ValueError("Expense file must remain inside the expenses directory")
    
  4. Reject all path separators, traversal components, null characters, and unexpected characters before constructing a filename.

  5. Add tests covering values such as ../../notes, absolute paths, mixed separators, malformed dates, invalid months, and traversal sequences contained in the first seven date characters.

T09 · Insecure Skill Coding Practices

Note
Location
scripts/log_expense.py:61
Finding

Markdown Table and Expense Record Injection

Content
View full analysis

Vulnerability Details

File Location: scripts/log_expense.py:61-66
Vulnerability Type: Markdown injection and stored record injection
Risk Level: Low

Vulnerable Code

python
# Create expense entry
entry = f"| {date} | {category} | {amount_str} | {description} | {tags} |\n"

# Append to file
with file_path.open("a") as f:
    f.write(entry)

Technical Analysis

The date, category, description, and tags fields are inserted directly into a Markdown table without escaping or validation. Newline characters allow an input value to terminate the current row and insert additional rows or arbitrary Markdown. Pipe characters can create extra cells and alter the table structure.

This is also a stored data-integrity issue because later calls to get_monthly_summary() parse table rows by splitting each line on |. A forged row containing a valid numeric amount can therefore be counted as a separate transaction.

Attack Path

  1. An attacker supplies a description containing a newline and a complete forged table row:
    bash
    python3 scripts/log_expense.py log 100 Coffee \
      --description $'legitimate entry\n| 2026-09-10 | Shopping | 999,999 | forged | injected |'
    
  2. The description is interpolated verbatim into entry.
  3. The newline ends the legitimate record and causes the forged row to be written as a separate Markdown table row.
  4. A later summary parses the forged row, adds 999,999 to the total, increments the transaction count, and attributes the amount to the forged category.
  5. Arbitrary Markdown can similarly be persisted in the ledger and displayed by Markdown-rendering software.

Impact Assessment

An attacker able to influence expense fields can:

  • Forge additional expense transactions.
  • Manipulate monthly totals, transaction counts, and category breakdowns.
  • Corrupt the Markdown table structure.
  • Persist arbitrary Markdown content ...[truncated 279 chars]
Remediation
View remediation

Remediation Suggestions

  1. Reject or normalize carriage returns and newline characters in all fields written to a single table row.

  2. Escape Markdown table delimiters, especially backslashes and pipe characters:

    python
    def escape_table_value(value: str) -> str:
        return (
            value.replace("\\", "\\\\")
                 .replace("|", "\\|")
                 .replace("\r", " ")
                 .replace("\n", " ")
        )
    
  3. Apply the escaping function to date, category, description, and tags before constructing the row.

  4. Validate dates through datetime.strptime() and impose reasonable length limits and accepted-character policies on categories, tags, and descriptions.

  5. Prefer storing authoritative records in a structured format such as JSON or SQLite. Generate Markdown as a presentation format from validated structured records instead of parsing user-editable Markdown as the source of truth.

  6. Add regression tests using embedded newlines, carriage returns, pipe characters, backslashes, and complete forged table rows. Confirm that each logging operation always produces exactly one parseable transaction.

Vulnerability Patterns
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Rogue AgentSelf-Modification, Session Persistence
  • 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 (4)

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
90% confidence
Finding

The skill instructs the agent to read and write files (expenses/ data and monthly summaries) but does not declare any explicit tool scope or permissions boundaries. Without a declared scope, an agent or runtime may grant broader file access than intended, increasing the risk of unauthorized reads/writes if the skill is invoked in an unexpected context.

Content

No source excerpt is available for this finding.

Session Persistence

Medium
Category
Rogue Agent
Confidence
60% confidence
Finding

Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Content

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

md
## Categories

See `references/categories.md` for common expense categories. Use existing categories or create custom ones as needed.

Common categories:
- **Housing** - Rent, utilities, home expenses

Description-Behavior Mismatch

Medium
Category
Not specified by scanner
Confidence
94% confidence
Finding

The script copies expense files to ~/Documents/expenses_backup before modification, which stores potentially sensitive financial data outside the configured workspace boundary. This creates unintended data replication to a broader user-accessible location, increasing exposure risk and violating least-surprise/isolation expectations for a workspace-scoped skill.

Content

No source excerpt is available for this finding.

Context-Inappropriate Capability

Medium
Category
Not specified by scanner
Confidence
91% confidence
Finding

The code performs cross-directory replication from the workspace into the user's Documents folder, which is unrelated to the core logging function and may expose personal finance records to other tools, sync clients, or users monitoring that location. In the context of an expense-tracking skill, financial entries are sensitive enough that silent duplication outside the intended storage area is a meaningful security and privacy issue.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.