T09 · Insecure Skill Coding Practices
- Location
scripts/usage-tracker.mjs:129- Finding
Predictable State File Allows Symbolic-Link File Overwrite
- Content
View full analysis
Vulnerability Details
File Location:
scripts/usage-tracker.mjs, lines 129–130, 318–321, and 327–339
Vulnerability Type: Unsafe file handling and symbolic-link following
Risk Level: MediumVulnerable Code:
javascript // Lines 129–130 this.storagePath = config.storagePath || './.usage-tracker.json'; this._load();javascript // Lines 318–321 if (fs.existsSync(this.storagePath)) { const data = JSON.parse(fs.readFileSync(this.storagePath, 'utf-8')); this.turns = data.turns || [];javascript // Lines 327–339 _save() { try { const data = { turns: this.turns, totalInputTokens: this.totalInputTokens, totalOutputTokens: this.totalOutputTokens, totalCacheCreation: this.totalCacheCreation, totalCacheRead: this.totalCacheRead, turnCount: this.turnCount, savedAt: new Date().toISOString() }; fs.writeFileSync(this.storagePath, JSON.stringify(data, null, 2), 'utf-8');Technical Analysis
The tracker uses the predictable relative path
./.usage-tracker.json, resolved against the process's current working directory. Before reading or writing this path, the implementation does not verify whether it is a regular file, reject symbolic links, validate ownership, or use a no-follow file-opening option.Node.js
fs.writeFileSync()follows an existing symbolic link and truncates the link target before writing the serialized tracker state. Therefore, an attacker who can create or replace.usage-tracker.jsonin a directory from which another user runs the tool can redirect the write to another file writable by the victim process.The
_load()method silently ignores loading failures. As a result, pointing the state path at a non-JSON target does not reliably stop exploitation: construction can continue, and a later state-changing operation such asrecord()orreset()invokes_save()and overwrites th ...[truncated 1643 chars]- Remediation
View remediation
Remediation Suggestions
- Store state in a dedicated per-user application-data directory rather than the current working directory. Ensure that the directory is owned by the invoking user and is not writable by untrusted users.
- Use
fs.lstatSync()or its asynchronous equivalent to reject an existing symbolic link and require an existing destination to be a regular file. - Open files with
O_NOFOLLOWwhere supported so the operating system rejects symbolic-link traversal. Do not rely exclusively on a check followed by a separate write, because that introduces a time-of-check/time-of-use race. - Create state files with restrictive permissions such as
0600. - Write updates to a securely created temporary file in the same trusted directory, flush and close it, and atomically rename it over the destination.
- Validate a caller-provided
storagePath: resolve it to an absolute path, constrain it to an approved directory where appropriate, and verify ownership and parent-directory permissions. - Report state-loading errors instead of silently ignoring them, particularly when the path is not a regular file or fails integrity validation.
