面试题答案
一键面试动态加载模块中的安全风险
- 模块路径可控导致代码注入:如果模块路径由用户输入或其他不可信来源控制,攻击者可能构造恶意路径,加载恶意代码模块,从而执行任意代码。例如,攻击者可以将路径指向包含恶意代码的本地文件或通过网络请求加载恶意模块。
- 依赖混淆:在动态加载模块时,可能会意外加载到恶意的同名模块。比如,攻击者发布一个恶意的npm包,其名称与项目依赖的某个常用包类似,在动态加载时可能误加载该恶意包。
防范风险的方法
- 安全策略:
- 白名单策略:维护一个允许加载的模块路径白名单。只允许从特定目录或特定npm包中加载模块,拒绝其他任何路径的模块加载请求。
- 权限控制:限制运行Node.js应用的用户权限,避免以root等超级用户权限运行,减少恶意代码注入后可能造成的危害。
- 代码审查:
- 检查模块路径来源:在代码审查过程中,重点检查模块路径的生成逻辑,确保路径来源可靠,不依赖不可信输入。
- 验证模块内容:审查加载模块的代码,确保模块没有包含恶意逻辑,特别是在加载第三方模块时,要查看其开源仓库、社区评价等,确认模块的安全性。
- 技术手段:
- 使用内置模块
path
:在处理模块路径时,使用Node.js内置的path
模块,通过path.join
等方法规范化路径,避免路径拼接过程中出现漏洞,如路径穿越漏洞。 - 验证模块签名:对于重要的模块,可以使用签名验证机制,确保模块的完整性和来源可信。例如,使用
node -t
标志对导入的ESM模块进行签名验证(Node.js v18+)。
- 使用内置模块
代码示例
- 存在风险的代码:
const { readFileSync } = require('fs');
const { join } = require('path');
const userInput = process.argv[2]; // 假设从命令行获取用户输入作为模块路径
const modulePath = join(__dirname, userInput);
const moduleCode = readFileSync(modulePath, 'utf8');
const moduleFunction = new Function('exports', moduleCode);
const exports = {};
moduleFunction(exports);
console.log(exports);
此代码中,userInput
来自用户输入,直接用于拼接模块路径,存在代码注入风险。攻击者可以输入恶意路径,如../evil.js
,加载恶意代码。
- 安全加固后的代码:
const { readFileSync } = require('fs');
const { join } = require('path');
// 定义白名单目录
const whitelistDir = __dirname;
const userInput = process.argv[2];
// 规范化路径
const normalizedPath = join(whitelistDir, userInput);
// 检查路径是否在白名单目录内
if (!normalizedPath.startsWith(whitelistDir)) {
throw new Error('Invalid module path');
}
const moduleCode = readFileSync(normalizedPath, 'utf8');
const moduleFunction = new Function('exports', moduleCode);
const exports = {};
moduleFunction(exports);
console.log(exports);
此代码通过定义白名单目录,并在加载模块前检查路径是否在白名单内,防止恶意路径加载,提高了安全性。同时,使用path.join
方法规范化路径,避免路径拼接漏洞。