121 lines
3.7 KiB
JavaScript
Raw Permalink Normal View History

/**
* 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
};