Back to skill

Security audit

Cat Food Tracker

Security checks for vulnerabilities and agentic risk

Overview

This skill is a focused local tool for validating and summarizing cat feeding backups, with minor CSV and oversized-input safety caveats but no hidden or disproportionate behavior.

Installers should treat backup contents as private, use trusted backup files when exporting CSV, and be cautious opening generated CSVs in spreadsheet software until formula neutralization is added. Avoid piping or loading very large untrusted files because size rejection happens after buffering.

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/cat-food-core.mjs:342
Finding

Spreadsheet Formula Injection in CSV Exports

Content
View full analysis
[pet.id, pet])); const petIds = new Set([ ...backup.pets.map((pet) => pet.id), ...backup.feed_records.map((record) => record.pet_id), ...backup.weight_records.map((record) => record.pet_id), ...backup.water_records.map((record) => record.pet_id), ]); const rows = []; for (const petId of [...petIds].sort()) { const pet = petById.get(petId) ?? { id: petId, name: '' }; ``` ```js rows.push({ pet_id: petId, pet_name: pet.name, date, ``` ```js export function toCSV(rows) { const headers = rows.length === 0 ? SUMMARY_HEADERS : Object.keys(rows[0]); const lines = rows.map((row) => headers.map((header) => JSON.stringify(row[header] ?? '')).join(',')); return [headers.join(','), ...lines].join('\n'); } ``` ### Technical Analysis The backup schema accepts arbitrary strings for fields such as `pet.id` and `pet.name`. These values are copied into summary rows and then serialized as CSV cells using `JSON.stringify`. JSON quoting provides delimiter and quote escaping, but it is not spreadsheet formula neutralization. Spreadsheet applications may interpret a cell beginning with `=`, `+`, `-`, or `@` as a formula even when that cell is quoted in the CSV file. Consequently, a crafted backup can inject spreadsheet expressions into `pet_id` or `pet_name`. The issue becomes exploitable when a generated CSV file is opened in software that evaluates formulas from imported CSV cells. Actual behavior depends on the spreadsheet product and its security settings. ### Attack Path 1. An attacker creates or modifies a backup containing ...[truncated 1679 chars]
Remediation
View remediation
{ if (typeof value !== 'string') return value; return /^[\s]*[=+\-@]/.test(value) ? `'${value}` : value; }; export function toCSV(rows) { const headers = rows.length === 0 ? SUMMARY_HEADERS : Object.keys(rows[0]); const lines = rows.map((row) => headers .map((header) => JSON.stringify(neutralizeSpreadsheetFormula(row[header] ?? ''))) .join(',') ); return [headers.join(','), ...lines].join('\n'); } ``` If preserving the exact original text is required, consider offering separate machine-oriented CSV and spreadsheet-safe CSV modes, with the safe mode used by default. ]]>

T09 · Insecure Skill Coding Practices

Note
Location
scripts/daily-summary.mjs:19
Finding

Input Size Limit Is Enforced Only After the Entire Input Is Buffered

Content
View full analysis
{ let text = ''; stdin.setEncoding('utf8'); stdin.on('data', (chunk) => { text += chunk; }); stdin.on('end', () => resolve(text)); stdin.on('error', reject); }); } return readFile(input, 'utf8'); } ``` The same pattern appears in `scripts/validate-backup.mjs:11-23`: ```js async function readInput(input) { if (!input || input === '-') { return new Promise((resolve, reject) => { let text = ''; stdin.setEncoding('utf8'); stdin.on('data', (chunk) => { text += chunk; }); stdin.on('end', () => resolve(text)); stdin.on('error', reject); }); } return readFile(input, 'utf8'); } ``` The documented limit is enforced later in `scripts/cat-food-core.mjs:197-205`: ```js export function parseBackupText(text) { if (typeof text !== 'string') fail('Backup input must be text'); if (text.length === 0) fail('Backup input is empty'); if (Buffer.byteLength(text, 'utf8') > MAX_BACKUP_FILE_BYTES) fail('Backup input exceeds 5 MB'); try { return JSON.parse(text); } catch { fail('Backup input is not valid JSON'); } } ``` ### Technical Analysis The core parser declares a 5 MB input limit, but both command-line entry points read the complete file or standard-input stream into a JavaScript string before calling `parseBackupText`. For file input, `readFile(input, 'utf8')` loads the entire file into memory. For standard input, every chunk is concatenated into an unbounded string. The byte-size check therefore occurs only ...[truncated 1378 chars]
Remediation
View remediation
MAX_BACKUP_FILE_BYTES) { throw new Error('Backup input exceeds 5 MB'); } chunks.push(buffer); } return Buffer.concat(chunks).toString('utf8'); } ``` Use equivalent bounded streaming for file input, or reject oversized regular files before reading them. ]]>
Vulnerability Patterns
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep