| 203 | static async invokeNvmInstall(projectDir: string): Promise<void>; |
| 204 | static async invokeNvmInstall(projectDir: string, quiet: boolean): Promise<void>; |
| 205 | static async invokeNvmInstall(projectDir: string, quiet = false): Promise<void> { |
| 206 | if (!existsSync(join(projectDir, '.nvmrc'))) { |
| 207 | return; |
| 208 | } |
| 209 | |
| 210 | try { |
| 211 | const nodeVersionFromNvmrc = readFileSync(join(projectDir, '.nvmrc'), 'utf8').trim(); |
| 212 | |
| 213 | // Validate .nvmrc content to prevent command injection/path hijacking |
| 214 | if ( |
| 215 | nodeVersionFromNvmrc.includes('/') || |
| 216 | nodeVersionFromNvmrc.includes('\\') || |
| 217 | nodeVersionFromNvmrc.includes('..') |
| 218 | ) { |
| 219 | throw new Error('Invalid .nvmrc content: contains path traversal characters'); |
| 220 | } |
| 221 | |
| 222 | const versionPattern = /^[v0-9.-]+$/; |
| 223 | const isValidSemverRange = semver.validRange(nodeVersionFromNvmrc) !== null; |
| 224 | if (!versionPattern.test(nodeVersionFromNvmrc) && !isValidSemverRange) { |
| 225 | throw new Error('Invalid .nvmrc content: does not match valid version pattern'); |
| 226 | } |
| 227 | |
| 228 | // We must source nvm.sh so the shell recognizes the 'nvm' command since nvm is not a binary |
| 229 | // but a shell function. The dot (.) built-in and && operator require shell: true here. |
| 230 | // We redirect stdout of nvm install to stderr so that stdout only contains the result of nvm which. |
| 231 | const {stdout} = await ChildProcess.spawn( |
| 232 | '. ~/.nvm/nvm.sh && nvm install >&2 && nvm which --silent', |
| 233 | [], |
| 234 | { |
| 235 | cwd: projectDir, |
| 236 | mode: 'on-error', |
| 237 | shell: true, |
| 238 | }, |
| 239 | ); |
| 240 | |
| 241 | const nodeBinPath = stdout.trim(); |
| 242 | if (nodeBinPath) { |
| 243 | const homeDir = os.homedir(); |
| 244 | const absoluteNodeBinPath = resolve(nodeBinPath); |
| 245 | const absoluteProjectDir = resolve(projectDir); |
| 246 | |
| 247 | if (!absoluteNodeBinPath.startsWith(homeDir)) { |
| 248 | throw new Error( |
| 249 | `Security Error: Resolved Node.js path is outside the home directory: ${nodeBinPath}`, |
| 250 | ); |
| 251 | } |
| 252 | if (absoluteNodeBinPath.startsWith(absoluteProjectDir)) { |
| 253 | throw new Error( |
| 254 | `Security Error: Resolved Node.js path is inside the project directory: ${nodeBinPath}`, |
| 255 | ); |
| 256 | } |
| 257 | |
| 258 | const nodeDir = dirname(absoluteNodeBinPath); |
| 259 | const currentPath = process.env['PATH'] || ''; |
| 260 | const pathParts = currentPath.split(':'); |
| 261 | |
| 262 | // Only update if the requested node directory is not already the first entry in PATH. |