T05 · Unauthorized Access and Privilege Escalation
- Location
nova-letters.js:39- Finding
Path Traversal Allows Reading Markdown Files Outside the Letter Store
- Content
View full analysis
Vulnerability Details
File Location:
nova-letters.js:39-42
Vulnerability Type: Path traversal and unauthorized local file disclosure
Risk Level: MediumVulnerable Code
js function readLetter(date) { const file = path.join(STORE, `${date || today()}.md`); if (!fs.existsSync(file)) { console.log(`No letter for ${date || today()}.`); return; } console.log('\n' + fs.readFileSync(file, 'utf8')); }Technical Analysis
The
dateargument is supplied through the command line and passed directly topath.join()without validation:js readLetter(args[1]);Although the documented input is a date in
YYYY-MM-DDformat, the implementation does not enforce this format. Node.js normalizes..path components when constructing the path, so a value containing directory traversal sequences can escape the intended~/.openclaw/workspace/lettersdirectory.The application automatically appends
.md, limiting direct exploitation to files whose names end in that extension. However, this can still expose Markdown files containing agent instructions, persistent memory, workspace records, or other sensitive information. The code also does not resolve and verify the canonical path, so symbolic links located inside the letter store could lead to files outside it.Attack Path
-
An attacker, untrusted automation, or injected agent instruction controls the date argument supplied to the CLI.
-
The attacker invokes a command containing traversal components, for example:
bash nova-letters read ../../HEARTBEAT -
The application constructs the path:
text ~/.openclaw/workspace/letters/../../HEARTBEAT.md -
path.join()normalizes the traversal components, producing a path outside the intendedlettersdirectory. -
If the resulting Markdown file exists and is readable by the current operating-system user,
fs.readFileSync()reads it an ...[truncated 1011 chars]
-
- Remediation
View remediation
Remediation Suggestions
Enforce the documented date format before constructing a filename:
js function readLetter(date = today()) { if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) { throw new Error('Invalid date. Expected YYYY-MM-DD.'); } const storeRoot = path.resolve(STORE); const file = path.resolve(storeRoot, `${date}.md`); if (path.dirname(file) !== storeRoot) { throw new Error('Requested file is outside the letter store.'); } if (!fs.existsSync(file)) { console.log(`No letter for ${date}.`); return; } const stat = fs.lstatSync(file); if (!stat.isFile() || stat.isSymbolicLink()) { throw new Error('Letter must be a regular file.'); } console.log('\n' + fs.readFileSync(file, 'utf8')); }Apply the following hardening measures:
- Accept only strict
YYYY-MM-DDinput and reject separators, traversal components, absolute paths, and malformed dates. - Resolve both the storage root and candidate path with
path.resolve()and verify that the candidate remains directly beneath the storage root. - Use
lstatSync()to reject symbolic links and non-regular files. - Where symlinks cannot be categorically rejected, compare canonical paths obtained using
fs.realpathSync()and ensure the target remains inside the canonical storage root. - Apply equivalent regular-file and canonical-path checks to the
listLetters()andsearch()file-reading paths. - Add regression tests covering
../, nested traversal, absolute paths, malformed dates, platform-specific separators, and symbolic links.
- Accept only strict
