T09 · Insecure Skill Coding Practices
Error
- Location
- src/io.js:15
- Finding
- Predictable Temporary File Allows Symlink-Based Arbitrary File Overwrite<![CDATA[ ## Vulnerability Details **File Location**: `src/io.js:15-27` **Vulnerability Type**: Predictable temporary file and unsafe symbolic-link handling **Risk Level**: High ### Vulnerable Code ```javascript export function atomicWrite(filePath, data) { fs.mkdirSync(path.dirname(filePath), { recursive: true }); const tmp = `${filePath}.tmp.${process.pid}.${Date.now()}`; const fd = fs.openSync(tmp, 'w'); try { fs.writeSync(fd, data); fs.fsyncSync(fd); fs.closeSync(fd); fs.renameSync(tmp, filePath); } catch (err) { try { fs.closeSync(fd); } catch {} try { fs.unlinkSync(tmp); } catch {} throw err; } } ``` ### Technical Analysis The temporary filename consists only of the destination path, process ID, and current timestamp. These values are predictable or can be approximated by another local process. The file is opened using the string flag `w`. This flag creates or truncates the referenced file, but it does not provide exclusive creation equivalent to `O_EXCL` and does not prevent symbolic-link traversal. If an attacker pre-creates the predicted temporary path as a symbolic link, `fs.openSync(tmp, 'w')` follows that link and truncates or overwrites the link target. The subsequent rename also moves the attacker-created symbolic link onto `filePath`. Consequently, exploitation can both modify an external file and leave the intended interchange path pointing to an attacker-selected destination. The issue affects direct calls to the exported `atomicWrite()` function and higher-level operations that invoke it, including `writeMd()` and index generation. ### Attack Path 1. A local attacker obtains write access to the directory containing the intended interchange file. 2. The attacker determines the victim process ID and estimates the timestamp at which `atomicWrite()` will run. 3. The attacker creates one or more symbolic links matching names such as: `target.md.tmp.<victim-pid>.<timestamp>`. 4. Each s ...[truncated 1059 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions - Generate temporary filenames using cryptographically secure randomness rather than timestamps alone, while keeping the temporary file in the destination directory to preserve atomic rename semantics. - Create the temporary file exclusively: ```javascript const tmp = `${filePath}.tmp.${process.pid}.${crypto.randomUUID()}`; const flags = fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL; const fd = fs.openSync(tmp, flags, 0o600); ``` - Add `O_NOFOLLOW` on platforms that expose and support it. - Before writing, use `lstatSync()` where appropriate to reject symbolic links. Treat this as defense in depth rather than a substitute for atomic exclusive creation. - Use restrictive temporary-file permissions such as `0o600`. - Verify that the opened object is a regular file using `fstatSync(fd)`. - Preserve cleanup in a `finally` block and track whether the descriptor has already been closed. - Add a regression test that pre-creates the candidate temporary path as a symbolic link and verifies that the external target remains unchanged. ]]>
