'use strict'; // safe-npm: the only place in this package that invokes child_process. // // Design constraints (see SECURITY_SCAN_NOTES.md, "Shell command execution"): // * subcommand is chosen from an immutable whitelist — callers can only // select by name, never by passing a string; // * every argv entry is a constant (no string concatenation from user input); // * spawnSync is called with `shell: false` and `windowsHide: true`; // * the child env is built from a small whitelist, so secrets in // process.env (tokens, OAuth state, etc.) never leak into the npm run; // * postinstall scripts are disabled unless the caller explicitly opts in. // // The only allowed binary is npm (or the fixed cmd.exe/npm.cmd wrapper Windows // requires for command shims). No wildcards or user-controlled argv: every npm // argument still comes from immutable constants below. const fs = require('fs'); const path = require('path'); const { spawnSync } = require('child_process'); const ALLOWED_SUBCOMMANDS = Object.freeze({ ci: Object.freeze(['ci', '--no-audit', '--no-fund']), install: Object.freeze(['install', '--no-audit', '--no-fund']), }); const SAFE_ENV_KEYS = Object.freeze([ 'PATH', 'Path', 'HOME', 'USERPROFILE', 'APPDATA', 'LOCALAPPDATA', 'SystemRoot', 'SYSTEMROOT', 'COMSPEC', 'TEMP', 'TMP', 'TMPDIR', 'LANG', 'LC_ALL', 'LC_CTYPE', 'HOMEDRIVE', 'HOMEPATH', 'PATHEXT', ]); function buildSafeEnv(sourceEnv, extra = {}) { const src = sourceEnv || process.env; const next = {}; for (const key of SAFE_ENV_KEYS) { if (src[key] !== undefined) next[key] = src[key]; } for (const [key, value] of Object.entries(src)) { if (key.startsWith('npm_config_')) next[key] = value; } for (const [key, value] of Object.entries(extra || {})) { if (value !== undefined) next[key] = value; } return next; } function detectPackageManager(targetDir) { if (fs.existsSync(path.join(targetDir, 'pnpm-lock.yaml'))) return 'pnpm'; if (fs.existsSync(path.join(targetDir, 'yarn.lock'))) return 'yarn'; if (fs.existsSync(path.join(targetDir, 'package-lock.json'))) return 'npm'; return null; } function runNpm(subcommand, targetDir, options = {}) { if (!Object.prototype.hasOwnProperty.call(ALLOWED_SUBCOMMANDS, subcommand)) { throw new Error(`safe-npm: subcommand "${subcommand}" is not in the allowlist`); } if (typeof targetDir !== 'string' || !targetDir) { throw new Error('safe-npm: targetDir must be a non-empty string'); } const baseArgs = ALLOWED_SUBCOMMANDS[subcommand].slice(); const allowPostinstall = Boolean(options.allowPostinstall); if (!allowPostinstall) baseArgs.push('--ignore-scripts'); const childEnv = buildSafeEnv(options.env || process.env, { npm_config_ignore_scripts: allowPostinstall ? 'false' : 'true', }); const bin = process.platform === 'win32' ? 'cmd.exe' : 'npm'; const args = process.platform === 'win32' ? ['/d', '/s', '/c', 'npm.cmd', ...baseArgs] : baseArgs; const result = spawnSync(bin, args, { cwd: targetDir, stdio: options.stdio || 'pipe', shell: false, windowsHide: true, env: childEnv, }); return { result, args: baseArgs }; } function safeNpmCi(targetDir, options = {}) { const { result, args } = runNpm('ci', targetDir, options); if (result.status !== 0) { const stderr = result.stderr ? String(result.stderr) : ''; const reason = result.error ? result.error.message : stderr; throw new Error(`npm ${args.join(' ')} 失败 (status=${result.status}): ${String(reason || '').slice(0, 500)}`); } return { ran: true, manager: 'npm', args }; } function safeNpmInstall(targetDir, options = {}) { const { result, args } = runNpm('install', targetDir, options); if (result.status !== 0) { const stderr = result.stderr ? String(result.stderr) : ''; const reason = result.error ? result.error.message : stderr; throw new Error(`npm ${args.join(' ')} 失败 (status=${result.status}): ${String(reason || '').slice(0, 500)}`); } return { ran: true, manager: 'npm', args }; } function installSkillDependencies(targetDir, options = {}) { const pkgJson = path.join(targetDir, 'package.json'); if (!fs.existsSync(pkgJson)) return { ran: false, manager: null }; const requireLockfile = options.requireLockfile !== false; const manager = detectPackageManager(targetDir); if (requireLockfile && manager !== 'npm') { throw new Error('安装拒绝执行:缺少 package-lock.json(开启 security.requireLockfile=false 可放宽)'); } const runOptions = { allowPostinstall: Boolean(options.allowPostinstall), stdio: options.stdio, env: options.env, }; const outcome = manager === 'npm' ? safeNpmCi(targetDir, runOptions) : safeNpmInstall(targetDir, runOptions); return { ran: true, manager: outcome.manager, allowPostinstall: runOptions.allowPostinstall, }; } module.exports = { ALLOWED_SUBCOMMANDS, SAFE_ENV_KEYS, buildSafeEnv, detectPackageManager, safeNpmCi, safeNpmInstall, installSkillDependencies, };