Back to skill

Security audit

Expense Tracker

Security checks for vulnerabilities and agentic risk

Overview

This expense tracker is purpose-aligned overall, but it needs review because it can persist sensitive spending data from ambiguous conversation and its helper script can be manipulated to read or write Markdown files outside the intended expenses folder.

Install only if you are comfortable with local markdown files storing your spending history. Use it only on trusted input, confirm before logging expenses from conversation, avoid sensitive details in descriptions or tags, and harden date/month validation before relying on it in shared or automated workflows.

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

Error
Location
scripts/log_expense.py:11
Finding

Path Traversal Through Unvalidated Date and Month Values

Content
View full analysis

Vulnerability Details

File Location: scripts/log_expense.py:11-22, with attacker-controlled values propagated at scripts/log_expense.py:35-40 and scripts/log_expense.py:117-134
Vulnerability Type: Path traversal and unintended file access
Risk Level: High

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"

The unvalidated path component is populated from both logging and summary arguments:

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
summary_parser.add_argument("year_month", nargs="?", help="Year-month (YYYY-MM, defaults to current month)")
summary_parser.add_argument("--workspace", help="Workspace path")
summary_parser.add_argument("--json", action="store_true", help="Output as JSON")
...
year_month = args.year_month or datetime.now().strftime("%Y-%m")
summary = get_monthly_summary(year_month, args.workspace)

Technical Analysis

get_expense_file() concatenates year_month directly into a filesystem path without validating that it conforms to YYYY-MM. It also does not resolve the resulting path and verify that it remains inside the intended expenses directory.

The summary command accepts year_month directl ...[truncated 2086 chars]

Remediation
View remediation

Remediation Suggestions

  • Validate logging dates by parsing them with datetime.strptime(date, "%Y-%m-%d").
  • Validate summary values with datetime.strptime(year_month, "%Y-%m").
  • Regenerate canonical date and month strings from parsed datetime objects instead of retaining the original input.
  • Resolve both the expense directory and candidate file path before access.
  • Verify that the resolved candidate is a direct child of the resolved expense directory.
  • Reject path separators, .., absolute paths, malformed dates, and unexpected suffixes.
  • Consider restricting --workspace to an explicitly approved root when arguments may originate from untrusted users.

Example hardening pattern:

python
def get_expense_file(year_month: str, workspace_path: str = None) -> Path:
    parsed = datetime.strptime(year_month, "%Y-%m")
    canonical_month = parsed.strftime("%Y-%m")

    workspace = (
        Path(workspace_path).expanduser().resolve()
        if workspace_path
        else (Path.home() / ".openclaw" / "workspace").resolve()
    )
    expenses_dir = (workspace / "expenses").resolve()
    expenses_dir.mkdir(parents=True, exist_ok=True)

    candidate = (expenses_dir / f"{canonical_month}.md").resolve()
    if candidate.parent != expenses_dir:
        raise ValueError("Expense path is outside the approved directory")

    return candidate

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/log_expense.py:35
Finding

Markdown Table and Expense-Record Injection

Content
View full analysis

Vulnerability Details

File Location: scripts/log_expense.py:35-52
Vulnerability Type: Stored Markdown injection and expense-record forgery
Risk Level: Medium

Vulnerable Code

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)
    
    # Initialize file if it doesn't exist
    if not file_path.exists():
        init_expense_file(file_path, year_month)
    
    # Format amount with thousand separators
    amount_str = f"{amount:,.0f}"
    
    # 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)

The generated file is later parsed using pipe delimiters:

python
for line in lines:
    if not line.strip() or line.startswith("|---"):
        continue
    
    parts = [p.strip() for p in line.split("|")[1:-1]]
    if len(parts) < 3:
        continue
    
    try:
        amount = float(parts[2].replace(",", ""))
        category = parts[1]
        
        total += amount
        by_category[category] = by_category.get(category, 0) + amount
        count += 1
    except (ValueError, IndexError):
        continue

Technical Analysis

The date, category, description, and tags fields are interpolated directly into a Markdown table row. The implementation neither rejects carriage returns and line feeds nor escapes Markdown table delimiters such as |.

A newline allows one logical input value to terminate the intended row and inject additional Markdown lines. If an injected line foll ...[truncated 1854 chars]

Remediation
View remediation

Remediation Suggestions

  • Reject \r and \n in every field stored in a single Markdown row.
  • Escape Markdown table delimiters, especially | and backslashes, before interpolation.
  • Enforce maximum lengths and explicit character policies for categories, descriptions, and tags.
  • Parse and canonicalize the date rather than writing the original date input.
  • Prefer a structured storage format such as JSON or CSV, using a standard serializer rather than manual delimiter concatenation.
  • If Markdown output is required, store canonical structured data and generate Markdown only for display.
  • When reading existing files, require exactly the expected number of columns and validate every field instead of accepting any row with at least three fields.

Example field hardening:

python
def escape_markdown_cell(value: str) -> str:
    if "\r" in value or "\n" in value:
        raise ValueError("Expense fields must not contain line breaks")
    return value.replace("\\", "\\\\").replace("|", "\\|")

Apply this function to category, description, and tags before constructing the row, while validating and canonicalizing date separately.

Vulnerability Patterns
  • Rogue AgentSelf-Modification, Session Persistence
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • 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
Findings (5)

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
95% confidence
Finding

The skill instructs the agent to read and write workspace files but does not declare any explicit tool scope or permissions boundary. This creates ambiguity around what filesystem access is expected and can enable over-broad file operations if the hosting agent grants more access than intended.

Content

No source excerpt is available for this finding.

Vague Triggers

Medium
Category
Not specified by scanner
Confidence
92% confidence
Finding

The description is broad enough to trigger on ordinary conversations about spending, summaries, or finance analysis, which can cause the skill to activate without a clear user request to persist data. Over-broad activation increases the chance of unintended tool use and storage of sensitive financial information.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
96% confidence
Finding

The skill stores personal financial data in markdown files but does not warn users that the information will be persisted in the workspace. Because expense entries may include sensitive categories, descriptions, dates, and spending patterns, silent storage can create privacy and data-retention risks.

Content

No source excerpt is available for this finding.

Session Persistence

Medium
Category
Rogue Agent
Confidence
67% confidence
Finding

The skill is explicitly designed for session persistence by maintaining expense records over time, so persistence itself is expected in context. However, because it involves ongoing storage of personal finance data and allows custom categories and descriptions, it still presents a privacy risk if retention is not clearly disclosed and bounded.

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

Vague Triggers

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The workflow says to act 'when the user mentions spending money,' which encourages automatic logging from conversational context rather than confirmed intent. This is dangerous because it can persist sensitive data based on ambiguous statements or inferred actions the user did not explicitly ask to record.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.