T09 · Insecure Skill Coding Practices
- Location
hooks/remem-flush/handler.js:57- Finding
Filesystem Boundary Bypass Through Symbolic Links in remem-flush Hook
- Content
View full analysis
Vulnerability Details
File Location:
hooks/remem-flush/handler.js, lines 57-59, 89-90, 103-104, and 147
Vulnerability Type: Symbolic-link following and arbitrary writable-file clobbering
Risk Level: HighVulnerable Code
javascript function writeJSON(filePath, data) { FS.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8'); }javascript const memoryDir = PATH.join(workspaceDir, 'memory'); const flushStatePath = PATH.join(memoryDir, 'flush-state.json');javascript const groupDir = PATH.join(memoryDir, 'groups', group); const attentionFile = PATH.join(groupDir, 'attention.md');javascript writeJSON(flushStatePath, newState);Group enumeration also follows symbolic links:
javascript return FS.readdirSync(groupsDir) .filter(f => { const stat = FS.statSync(PATH.join(groupsDir, f)); return stat.isDirectory(); });Technical Analysis
The hook derives paths from the configured workspace but does not verify that their canonical targets remain inside the workspace memory directory.
FS.statSync()follows symbolic links during group discovery, whileFS.writeFileSync()follows an existing symbolic link atmemory/flush-state.json.There is no use of
lstatSync(),realpathSync(), canonical path containment checks, or no-follow file-opening semantics. Consequently, possession of write access to the workspace is sufficient to redirect hook filesystem operations outside the intendedmemoryhierarchy.A symbolic-link group directory can cause the hook to inspect matching Markdown files outside
memory/groups. Their canonical paths and modification times can then be recorded inflush-state.json. More critically, replacingmemory/flush-state.jsonwith a symbolic link causes the hook to overwrite the link target with generated JSON.Attack Path
- An attacker obtains the ability to create or replace file ...[truncated 1473 chars]
- Remediation
View remediation
Remediation Suggestions
- Use
FS.lstatSync()for group entries and reject every symbolic link before treating an entry as a directory. - Resolve both the intended memory root and each accessed path with
FS.realpathSync(), then verify that the resolved target is equal to or nested beneath the canonical memory root. - Refuse to write when
flush-state.jsonalready exists as a symbolic link, directory, device, or other non-regular file. - Open the destination with no-follow and exclusive semantics where supported, such as
O_NOFOLLOW, rather than passing the path directly towriteFileSync(). - Write to a newly created temporary regular file in the same validated directory, set restrictive permissions such as
0600, callfsyncwhen durability matters, and atomically rename it over the validated destination. - Revalidate the destination immediately before replacement to reduce time-of-check/time-of-use race conditions.
- Run the hook under a dedicated low-privilege account with write access limited to the intended workspace memory directory.
- Use
