T09 · Insecure Skill Coding Practices
Error
- Location
- src/lib/storage.ts:44
- Finding
- Path Traversal Through Unvalidated Date Values Permits JSON File Access Outside the Log Directory<![CDATA[ ## Vulnerability Details **File Location**: `src/lib/storage.ts:44-53`; input reaches these functions without format validation through `src/commands/meal.ts:68-77` and `src/commands/meal.ts:93-104` **Vulnerability Type**: Path traversal and arbitrary file overwrite **Risk Level**: High ### Vulnerable Code ```ts function logPath(date: string): string { return join(LOGS_DIR, `${date}.json`); } export function readDayLog(date: string): DayLog { return readJson<DayLog>(logPath(date), []); } export function writeDayLog(date: string, log: DayLog): void { writeJson(logPath(date), log); } ``` The caller only verifies that the date and time values are present: ```ts function requireDate(args: Args): string { const d = args['date'] as string | undefined; if (!d) { err('--date YYYY-MM-DD is required'); process.exit(1); } return d; } function requireTime(args: Args): string { const t = args['time'] as string | undefined; if (!t) { err('--time HH:MM is required'); process.exit(1); } return t; } ``` A meal operation subsequently uses the unvalidated date: ```ts async function mealAdd(args: Args): Promise<void> { const date = requireDate(args); const time = requireTime(args); const name = args['name'] as string | undefined; if (!name) { err('--name is required'); process.exit(1); } const id = nanoid(8); const meal: Meal = { id, time, name, ingredients: [] }; const log = readDayLog(date); log.push(meal); writeDayLog(date, log); await upsertVector(mealVecId(id), name, { type: 'meal', mealId: id, date }); print({ id }); } ``` ### Technical Analysis The command documentation specifies dates in `YYYY-MM-DD` format, but `requireDate()` only checks whether a value is present. It does not validate the date syntax, reject path separators, or verify that the resolved path remains under `LOGS_DIR`. `logPath()` concatenates the attacker-controlled value with `.json` and passes it to `path.join()`. A value containing `../` ...[truncated 2200 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Enforce the documented format before any filesystem operation: ```ts function requireDate(args: Args): string { const value = args.date; if (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(value)) { throw new Error('--date must use YYYY-MM-DD'); } const parsed = new Date(`${value}T00:00:00Z`); if ( Number.isNaN(parsed.getTime()) || parsed.toISOString().slice(0, 10) !== value ) { throw new Error('invalid calendar date'); } return value; } ``` 2. Apply equivalent strict validation to time values, such as `^(?:[01]\d|2[0-3]):[0-5]\d$`. 3. Add defense-in-depth containment in the storage layer: ```ts import { dirname, resolve, sep } from 'path'; function logPath(date: string): string { const logsRoot = resolve(LOGS_DIR); const candidate = resolve(logsRoot, `${date}.json`); if (dirname(candidate) !== logsRoot) { throw new Error('invalid log path'); } return candidate; } ``` 4. Never rely solely on documentation to constrain filesystem-bound input. 5. Add automated tests for `../`, absolute paths, encoded separators, malformed dates, impossible calendar dates, and valid boundary dates. ]]>
