T05 · Unauthorized Access and Privilege Escalation
Error
- Location
- scripts/deploy.js:382
- Finding
- Symbolic Links Allow Access Outside the Approved Shared Directory<![CDATA[ ## Vulnerability Details **File Location**: `scripts/deploy.js:382-386`, `scripts/deploy.js:536-557`, and `scripts/deploy.js:562-567` **Vulnerability Type**: Symbolic-link directory confinement bypass **Risk Level**: High ### Vulnerable Code ```js function safeJoin(base, decoded) { const abs = path.normalize(path.join(base, decoded)); if (abs !== base && !abs.startsWith(base + path.sep)) return null; // 防目录穿越 return abs; } ``` The returned lexical path is subsequently accepted and followed by filesystem operations: ```js function resolveStatic(decoded) { const rel = decoded.replace(/^\/+/, ''); const p = safeJoin(root, rel); if (p) { const t = fileOrIndex(p); if (t) return { abs: t, template: false }; } if (runtimeOk) { const pr = safeJoin(runtimeDir, rel); if (pr) { const t = fileOrIndex(pr); if (t) return { abs: t, template: rel === 'index.html' && !selfContained }; } } return null; } function fileOrIndex(p) { try { const st = fs.statSync(p); if (st.isFile()) return p; if (st.isDirectory()) { const idx = safeJoin(p, 'index.html'); if (idx && fs.existsSync(idx)) return idx; } } catch (e) { /* 不存在 */ } return null; } ``` ### Technical Analysis `safeJoin()` prevents conventional `../` traversal only by normalizing and comparing the lexical path. It does not resolve the canonical filesystem path. Node.js operations used later—including `fs.statSync()`, `fs.existsSync()`, and `fs.createReadStream()`—follow symbolic links. Consequently, a path that appears to be beneath the shared root can resolve to an arbitrary file or directory outside that root. For example, a symlink named `leak` beneath the shared directory may point to a user home directory or credential directory. The path `<shared-root>/leak/id_rsa` passes the lexical prefix check even though its canonical target is outside `<shared-root>`. This behavior exceeds the user-approved directory scope and ...[truncated 1400 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Canonicalize the configured root once at startup: ```js const canonicalRoot = fs.realpathSync(root); ``` 2. Canonicalize every requested target before serving it: ```js function canonicalContainedPath(rootReal, candidate) { let targetReal; try { targetReal = fs.realpathSync(candidate); } catch { return null; } if ( targetReal !== rootReal && !targetReal.startsWith(rootReal + path.sep) ) { return null; } return targetReal; } ``` 3. Use the validated canonical path, rather than the original lexical path, for `stat` and streaming operations. 4. Reject symbolic links during directory traversal with `fs.lstatSync()` when symlink support is not required. 5. Repeat canonical containment validation immediately before opening the file to reduce time-of-check/time-of-use exposure. 6. Add regression tests covering: - Symlinks to external files - Symlinks to external directories - Nested symlink chains - Broken links - Links replaced between validation and access - Platform-specific junctions and reparse points on Windows ]]>
