T05 · Unauthorized Access and Privilege Escalation
Error
- Location
- scripts/lib/common.js:54
- Finding
- Symbolic Link Traversal Allows Access Outside the Configured Vault<![CDATA[ ## Vulnerability Details **File Location**: `scripts/lib/common.js:54-65`; write impact occurs through `scripts/append_note.js:58-66` **Vulnerability Type**: Symbolic link traversal and insufficient filesystem boundary validation **Risk Level**: High ### Vulnerable Code ```js function readVaultDir(dir, files = [], ignorePatterns = null) { if (!fs.existsSync(dir)) return files; const patterns = ignorePatterns || loadIgnorePatterns(); for (const item of fs.readdirSync(dir)) { const fullPath = path.join(dir, item); if (shouldIgnore(fullPath, patterns)) continue; const stat = fs.statSync(fullPath); if (stat.isDirectory()) readVaultDir(fullPath, files, patterns); else if (item.endsWith('.md')) files.push(fullPath); } return files; } ``` The discovered path is subsequently trusted by the append operation: ```js const filePath = findNoteFile(data.title); if (!filePath) return { error: `Note not found: ${data.title}` }; const append = buildAppendBlock(data); if (append.error) return { error: append.error }; let content = fs.readFileSync(filePath, 'utf-8').replace(/\s*$/, ''); content += append.block; content = updateFrontmatterUpdated(content, new Date().toISOString().split('T')[0]); fs.writeFileSync(filePath, content, 'utf-8'); ``` ### Technical Analysis The recursive vault scanner uses `fs.statSync`, which follows symbolic links. It does not use `fs.lstatSync` to identify and reject links, nor does it canonicalize candidate paths with `fs.realpathSync` and verify that the resulting path remains beneath the canonical vault root. Consequently, a Markdown-named symbolic link inside the vault can reference a file outside the vault. A symbolic link to an external directory can likewise cause the scanner to recurse through external directories and collect their Markdown files. All read-oriented features that rely on `readVaultDir`, including search, backlink lookup, context-pack generation, related-note discovery, and link ...[truncated 2102 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Canonicalize the vault root once: ```js const VAULT_REAL_PATH = fs.realpathSync(VAULT_PATH); ``` 2. Use `fs.lstatSync` before following any directory entry and reject symbolic links unless the product explicitly needs them: ```js const stat = fs.lstatSync(fullPath); if (stat.isSymbolicLink()) continue; ``` 3. Canonicalize every candidate file or directory and enforce containment: ```js function assertInsideVault(candidate) { const real = fs.realpathSync(candidate); if (real !== VAULT_REAL_PATH && !real.startsWith(VAULT_REAL_PATH + path.sep)) { throw new Error('Path resolves outside the configured vault'); } return real; } ``` 4. Perform the containment check immediately before every read and write, not only during initial discovery. This reduces time-of-check/time-of-use exposure if filesystem entries are replaced after scanning. 5. Track visited canonical directory paths during recursion to prevent symbolic-link cycles, bind mounts, or repeated traversal. 6. For writes, open the validated target defensively and reject symbolic links at the final path component where platform support permits. Revalidate the parent directory and target directly before writing. 7. Add tests covering file symlinks, directory symlinks, links escaping through multiple levels, recursive cycles, and replacement of a validated file with a link before append. ]]>
