/** * GMP-Agent 路径与名称安全守卫 * 工单编号: GH-GMP-004 · CodeQL hardening * 职责: 防止命令注入和路径穿越 * * 两道防线: * 1. assertSafeModuleName: 模块名白名单, 拒绝包含特殊字符的输入 * 2. assertWithinBase: path.join 后 resolve+relative 校验, 拒绝逃出 baseDir 的路径 */ 'use strict'; const path = require('path'); /** * 模块名白名单正则 * - 允许字母数字、下划线、横杠、点号 * - 长度 1~64 * - 禁止以点号开头 (避免 . / .. / .git 等隐藏路径) * - 禁止包含连续点号 (避免 .. 路径穿越) */ const MODULE_NAME_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9_\-.]{0,63}$/; /** * 校验模块名格式 * @param {string} name * @throws {Error} 不合法时抛出 */ function assertSafeModuleName(name) { if (typeof name !== 'string' || name.length === 0) { throw new Error('非法模块名: 必须为非空字符串'); } if (!MODULE_NAME_REGEX.test(name)) { throw new Error('非法模块名: ' + JSON.stringify(name) + ' (仅允许字母数字下划线横杠点号, 长度1~64)'); } if (name.indexOf('..') !== -1) { throw new Error('非法模块名: 禁止包含连续点号'); } } /** * 标准化模块名: 校验 + path.basename 提取最后一段, 强行去除任何路径分隔符 * 同时是 CodeQL 公认的 path-injection sanitizer * @param {string} name * @returns {string} 净化后的模块名 */ function sanitizeModuleName(name) { assertSafeModuleName(name); const sanitized = path.basename(name); // path.basename 本身去除路径分隔符, 与白名单二次校验保险 if (sanitized !== name) { throw new Error('非法模块名: 包含路径分隔符 ' + JSON.stringify(name)); } return sanitized; } /** * 校验命令行参数不以 '-' 开头, 防止 git/pm2 等命令的二阶选项注入 * (例如 --upload-pack=evil 会让 git 执行任意命令) * @param {string} arg * @param {string} label - 参数名, 用于错误信息 */ function assertNotOption(arg, label) { if (typeof arg !== 'string' || arg.length === 0) { throw new Error('非法' + label + ': 必须为非空字符串'); } if (arg.startsWith('-')) { throw new Error('非法' + label + ': 禁止以 "-" 开头 (防选项注入) value=' + JSON.stringify(arg.substring(0, 40))); } } /** * 校验 git 仓库 URL 格式 (允许 https / git / ssh / file) * @param {string} url */ function assertSafeGitUrl(url) { assertNotOption(url, 'git URL'); // 仅允许常见协议前缀 const allowed = /^(https?:\/\/|git:\/\/|git@|ssh:\/\/|file:\/\/)/; if (!allowed.test(url)) { throw new Error('非法 git URL: 必须以 https:// / git:// / git@ / ssh:// / file:// 开头'); } } /** * 校验目标路径在 baseDir 之内 (防路径穿越) * @param {string} baseDir - 受信任的根目录 * @param {string} targetPath - 待校验的路径 * @throws {Error} 逃出 baseDir 时抛出 */ function assertWithinBase(baseDir, targetPath) { const resolvedBase = path.resolve(baseDir); const resolvedTarget = path.resolve(targetPath); const rel = path.relative(resolvedBase, resolvedTarget); if (rel.startsWith('..') || path.isAbsolute(rel)) { throw new Error('非法路径: 目标路径逃出受信任根目录 base=' + resolvedBase + ' target=' + resolvedTarget); } } /** * 安全的 path.join: 拼接后立即校验未逃出 baseDir * @param {string} baseDir * @param {...string} parts * @returns {string} */ function safeJoin(baseDir, ...parts) { const joined = path.join(baseDir, ...parts); assertWithinBase(baseDir, joined); return joined; } module.exports = { MODULE_NAME_REGEX, assertSafeModuleName, sanitizeModuleName, assertNotOption, assertSafeGitUrl, assertWithinBase, safeJoin };