拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Node.js 安全指南:为什么必须避免 eval、new Function 等动态代码执行

Node.js 安全指南:为什么必须避免 eval、new Function 等动态代码执行
  • 文档
  • 教程
  • 后端

【免费下载链接】nodebestpractices

✅ The Node.js best practices list (July 2026)

项目地址:https://gitcode.com/GitHub_Trending/no/nodebestpractices
点击查看免费下载

本文依据当前仓库 nodebestpractices 中《Avoid JS eval statements》系列文档(英文原版、中文版、俄文版)整理成文。该章节对应 README 主清单中6.15 节 "Avoid JavaScript eval statements",并关联 OWASP Top 10 2017 的 A1 注入与 A7 跨站脚本(XSS) 两大威胁类别。

导读

在 Node.js 中,eval()、new Function()、setTimeout()与setInterval()这四个全局函数都能接收"字符串形式的 JavaScript 代码"并将其执行。本指南聚焦于这一高危模式:当不可信的用户输入流入这些函数时,等同于把服务器的执行权限拱手让给攻击者。读完本文,你将掌握:这些函数为什么危险、真实的攻击载荷长什么样、如何用 ESLint 安全插件在编码阶段自动拦截(detect-eval-with-expression规则),以及在不损失功能的前提下安全重构的替代方案。

一、风险核心:四个能"把字符串变成代码"的全局函数

根据 avoideval.md 的定义,eval()、setTimeout()、setInterval()和new Function()都是 JavaScript/Node.js 中的全局函数,它们共同的特征是:接受一个字符串参数,该字符串代表一段 JavaScript 表达式、语句或语句序列,随后在运行时被解析并执行。

从安全角度看,问题的本质并不是"函数本身有 bug",而是不可信的用户输入可能顺着数据流进入代码执行阶段。由于"执行用户提供的代码"本质上等于允许攻击者执行你(服务进程)所能执行的一切操作,其后果往往是服务器被完全攻陷(RCE,远程代码执行)。

README 主清单 6.15 节 给出的 TL;DR 进一步强调了两个维度:

  • 性能层面:eval让引擎无法对动态代码做编译期优化;
  • 安全层面:eval允许在运行时执行自定义 JavaScript,而恶意代码可能恰恰来源于用户输入。

同时它明确划出了红线:new Function构造函数同样应当避免;setTimeout与setInterval永远不应被传入动态 JavaScript 字符串。

二、攻击场景演示:一条字符串如何删掉整个服务器

avoideval.md 给出了一个极具说明性的恶意载荷示例。攻击者只需把下面这段字符串作为输入提交,一旦服务端不加过滤地把它交给eval(),就会在 Node.js 进程中执行任意系统命令:

// 攻击者能够注入的恶意代码示例 const userInput = "require('child_process').spawn('rm', ['-rf', '/'])"; // 恶意代码被执行 eval(userInput);

逐行拆解这条攻击链:

  1. 攻击者在某个可注入的输入点(如表单字段、URL 参数、消息队列消息、配置文件)写入字符串"require('child_process').spawn('rm', ['-rf', '/'])";
  2. 应用代码把该字符串原样传给eval();
  3. eval()把字符串当作 JavaScript 源码解析执行,require('child_process')在 Node.js 运行时环境中能够成功解析;
  4. child_process.spawn('rm', ['-rf', '/'])在操作系统层面发起递归删除根目录的进程。

需要特别强调的是:被执行的代码运行在与你应用相同的进程、相同的权限上下文里。它不仅能访问require、process、fs等 Node.js 全局能力,还能读取环境变量中的密钥、连接数据库、向外部发起请求。eval之后的任何防护都无从谈起——这正是它被称为"注入之王"的原因。

三、为什么 eval 被安全界"深恶痛绝":专家视角与底层原理

3.1 Liran Tal 的论断

avoideval.md 引用了 Liran Tal 所著《Essential Node.js Security》中的原话:

从安全角度而言,eval()或许是 JavaScript 中最令人皱眉的组成部分。它把一段 JavaScript 字符串当作文本解析,然后像执行 JavaScript 代码一样执行它。当不可信的用户输入有机会流入eval()时,灾难的配方就此形成,最终可能导致服务器被攻陷。

这段话精准地概括了两点:其一,eval是"文本 → 可执行代码"的转换器;其二,危险不在于eval本身,而在于不可信输入与eval的相遇。

3.2 底层原理:为什么动态执行难以防御

从 V8 引擎实现角度可以进一步解释其危险性:

  • 无法静态分析:eval的输入在编译期不可见,引擎无法对其做任何静态安全检查、优化或死代码消除,这在 README.md 中被明确指出是性能隐患;
  • 作用域泄漏:直接调用eval时,被求值的代码能访问当前调用函数的局部变量,等于把内部状态暴露给任意字符串;
  • 绕过所有输入校验:应用层做的白名单、黑名单、类型校验全部发生在执行之前,一旦字符串到达eval,之前的校验结果对执行结果没有任何约束力;
  • 环境权限巨大:在 Node.js 中,被求值代码可以访问require、process.env、fs、net等全部运行时能力,其攻击面远超浏览器环境。

3.3 容易被忽视的"同族函数"

eval是最著名的一个,但下面这几种写法同样危险,且更容易在代码评审中被漏掉:

// 用 new Function 构造可执行函数 const fn = new Function('return ' + userInput); fn(); // 把用户输入作为 setTimeout / setInterval 的"代码字符串" setTimeout(userInput, 100); setInterval("doSomething(" + userInput + ")", 1000);

其中setTimeout/setInterval的字符串参数形态尤其隐蔽——它们的第一个参数既可以传函数(安全),也可以传字符串(会被当作代码执行),开发者稍不注意就会把用户输入拼接进去。README 6.15 节 明确要求:setTimeout 和 setInterval 永远不要传入动态 JavaScript 代码。

四、工程防线:用安全 Linter 在编码阶段拦截 eval

既然动态执行如此危险,最佳防线就是在代码写入阶段就将其扼杀。仓库 lintrules.md("使用 linter 安全规则"章节)提供了现成的检测方案:eslint-plugin-security。

该插件基于一系列已知漏洞模式做静态检查,其中与本文主题直接对应的规则是detect-eval-with-expression,它会精准捕获"把非常量表达式传给eval"的写法:

// 会被 detect-eval-with-expression 规则拦截的代码 const userinput = req.body.userinput; eval(userinput);

规则命中的关键特征是eval的实参不是字符串字面量(即来自外部输入或变量),这与我们的风险场景完全吻合。同一插件还包含detect-pseudoRandomBytes(伪随机数)、detect-non-literal-fs-filename(非字面量文件路径)、detect-non-literal-regexp(动态正则)等规则,lintrules.md 中给出了这些不安全模式的完整示例代码。

上图是 lintrules.md 章节中eslint-plugin-security对上述不安全代码实际运行检测的示例输出。将该插件接入 CI 或 git 钩子(如 pre-git),可以在代码推送到远端之前强制拦截eval、动态new Function等危险模式,与本文倡导的"从源头杜绝动态执行"形成完整的工程闭环。

五、安全重构:不依赖动态执行完成同样的功能

avoideval.md 给出的核心建议是:重构代码,使其不依赖这些函数,尤其是在用户输入可能被传入并执行的场景下。下面给出常见场景的替代方案:

5.1 解析 JSON / 配置 → 用专用解析器,别用 eval

// 危险:把用户输入当表达式求值 const config = eval('(' + userInput + ')'); // 安全:使用严格模式下的 JSON 解析器 const config = JSON.parse(userInput);

JSON.parse只解析数据、不执行代码,且会拒绝函数、__proto__污染等危险载荷,是解析用户提交结构化数据的首选。

5.2 定时任务 → 永远传函数引用,不传字符串

// 危险:字符串作为代码执行 setTimeout('updateStatus(' + userId + ')', 1000); // 安全:传入函数引用 setTimeout(() => updateStatus(userId), 1000);

同样地,setInterval也一律使用函数参数形式。

5.3 模板/表达式求值 → 用安全的解释器或映射表

若业务确实需要"按规则计算"(如动态公式、报表表达式),应选用不调用eval的专用表达式解析库,或改用白名单映射:

// 用查找表替代 eval 分支 const handlers = { sum: (a, b) => a + b, avg: (a, b) => (a + b) / 2, }; const result = handlers[userInput.operator]?.(userInput.a, userInput.b);

5.4 动态调用模块 → 参考仓库的 safemoduleloading 实践

如果需求是"按变量加载模块",这与仓库中 safemoduleloading.md(6.17 节"避免使用变量进行模块加载")直接相关:require(userInput)同样会把用户输入引入代码执行路径。正确做法是建立白名单目录或使用显式映射,绝不让用户输入直接决定加载哪个模块。相关风险还包括 regex.md(6.16 节"防止恶意正则拖垮单线程")与 childprocesses.md(子进程安全)——它们共同构成了"杜绝不可信输入进入任意执行路径"的安全原则族。

六、总结:把"动态执行"从代码库里彻底清除

回顾 avoideval.md 及其系列译本的核心结论:

函数危险形态正确替代
eval()用户输入作为代码执行JSON.parse、白名单映射
new Function()用户输入构造可执行函数显式函数、专用解析库
setTimeout(str, ms)字符串参数被当作代码传函数引用
setInterval(str, ms)字符串参数被当作代码传函数引用

在实际项目中,请把以下三条作为强制约束:第一,代码评审中把eval、new Function、字符串形式的setTimeout/setInterval视为必须整改的红线;第二,接入eslint-plugin-security并在 CI/提交钩子中强制执行detect-eval-with-expression,参考 lintrules.md;第三,对"必须动态计算"的少数业务场景,坚持"先白名单、后解析、绝不直接执行用户字符串"的原则。做到这三点,注入类攻击在本项目中最大的入口之一便被彻底封死。

  • 文档
  • 教程
  • 后端

【免费下载链接】nodebestpractices

✅ The Node.js best practices list (July 2026)

项目地址:https://gitcode.com/GitHub_Trending/no/nodebestpractices
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

返回列表