* Resolve a user-supplied path safely against `cwd`. * Returns { ok: true, fullPath, displayPath } on success, * or { ok: false, reason } on rejection. * * Rules: * - Resolved absolute path must be inside `cwd` (unless allowOutside=true) * - Path must not match a sensitive pattern * - P
(reqPath, cwd, options = {})
| 123 | * - Path must not contain NUL bytes |
| 124 | */ |
| 125 | function safeResolvePath(reqPath, cwd, options = {}) { |
| 126 | if (typeof reqPath !== 'string' || reqPath.length === 0) { |
| 127 | return { ok: false, reason: 'path must be a non-empty string' }; |
| 128 | } |
| 129 | if (reqPath.indexOf('\u0000') !== -1) { |
| 130 | return { ok: false, reason: 'path contains NUL byte' }; |
| 131 | } |
| 132 | // Expand ~ for the user's home directory only when explicitly allowed |
| 133 | let candidate = reqPath; |
| 134 | if (candidate === '~' || candidate.startsWith('~/') || candidate.startsWith('~\\')) { |
| 135 | if (!options.allowHome) { |
| 136 | return { ok: false, reason: 'home-relative paths are blocked' }; |
| 137 | } |
| 138 | candidate = path.join(os.homedir(), candidate.slice(2)); |
| 139 | } |
| 140 | // Strip leading ./ for normalization |
| 141 | candidate = candidate.replace(/^\.[/\\]/, ''); |
| 142 | const fullPath = path.resolve(cwd, candidate); |
| 143 | |
| 144 | // Sensitive path check |
| 145 | for (const re of SENSITIVE_PATH_RE) { |
| 146 | if (re.test(fullPath)) { |
| 147 | return { ok: false, reason: 'path is sensitive (auth credentials)' }; |
| 148 | } |
| 149 | } |
| 150 | |
| 151 | // Containment check (default ON — opt out for tools that legitimately |
| 152 | // need to read outside the project, like reading user's global config) |
| 153 | if (!options.allowOutside) { |
| 154 | const normCwd = path.resolve(cwd); |
| 155 | const rel = path.relative(normCwd, fullPath); |
| 156 | if (rel.startsWith('..') || path.isAbsolute(rel)) { |
| 157 | return { ok: false, reason: 'path resolves outside project root' }; |
| 158 | } |
| 159 | } |
| 160 | |
| 161 | const displayPath = path.relative(cwd, fullPath) || path.basename(fullPath); |
| 162 | return { ok: true, fullPath, displayPath }; |
| 163 | } |
| 164 | |
| 165 | // ─── Shell escaping ───────────────────────────────────────────────────────── |
| 166 |
no outgoing calls
no test coverage detected