T05 · Unauthorized Access and Privilege Escalation
Error
- Location
- scripts/audit-skill-dir.mjs:28
- Finding
- Symbolic Link Following Allows Reads Outside the Selected Audit Directory<![CDATA[ ## Vulnerability Details **File Location**: `scripts/audit-skill-dir.mjs`, lines 28–41 **Vulnerability Type**: Filesystem boundary violation through symbolic-link following **Risk Level**: High ### Vulnerable Code ```js function walk(dir) { for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { if (entry.name === '.git' || entry.name === 'node_modules' || entry.name === '.next' || entry.name === 'dist') continue; const p = path.join(dir, entry.name); if (entry.isDirectory()) walk(p); else if (includeExt.has(path.extname(entry.name).toLowerCase()) || entry.name === 'SKILL.md' || entry.name === 'package.json') scanFile(p); } } function scanFile(file) { const text = fs.readFileSync(file, 'utf8'); const lines = text.split(/\r?\n/); lines.forEach((line, i) => { for (const rule of rules) { if (rule.re.test(line)) { findings.push({ level: rule.level, label: rule.label, file, line: i + 1, excerpt: line.trim().slice(0, 200) }); } } }); } ``` ### Technical Analysis The directory auditor assumes that every file discovered beneath the selected directory is physically contained within that directory. However, it does not use `lstatSync` to identify symbolic links and does not compare each file's canonical path against the canonical audit root. For a symbolic link, `Dirent.isDirectory()` is false, so a link whose name has an accepted extension can reach `scanFile`. `fs.readFileSync` then follows the symbolic link and reads its external target. The scanner does not print entire files, but any external line matching a detection rule can be included in the JSON output as a finding excerpt. Large or unsuitable linked files may also cause resource consumption or scanner failure. ### Attack Path 1. An attacker prepares a skill directory containing a symbolic link with an accepted filename, such as `external.md`. 2. The symbolic link points to a file outside the skill directory that is readable b ...[truncated 812 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions - Canonicalize the audit root with `fs.realpathSync`. - Inspect entries with `fs.lstatSync` and reject symbolic links by default. - Canonicalize every candidate file before reading it. - Require each canonical candidate path to equal the canonical root or begin with `canonicalRoot + path.sep`. - Open files with protections against symbolic-link following where supported, such as `O_NOFOLLOW`. - Impose maximum file-size and total-scan-size limits. - Handle filesystem race conditions and read errors without exposing sensitive content. Example containment logic: ```js const canonicalRoot = fs.realpathSync(root); function assertContained(candidate) { const stat = fs.lstatSync(candidate); if (stat.isSymbolicLink()) { throw new Error(`Symbolic links are not allowed: ${candidate}`); } const canonical = fs.realpathSync(candidate); if (canonical !== canonicalRoot && !canonical.startsWith(canonicalRoot + path.sep)) { throw new Error(`Path escapes audit root: ${candidate}`); } return canonical; } ``` ]]>
