本文目录导读:

- 方案一:自定义领域特定语言(DSL) —— 最安全
- 方案二:使用 PHP 沙箱扩展 —— 中等安全
- 方案三:编译成其他安全语言 —— 高级安全
- 方案四:纯
eval并配合严格的disable_functions—— 高风险 - 最佳实践建议
这是一个相当复杂但也非常有吸引力的需求,在PHP项目中实现用户自定义脚本,核心挑战在于安全(沙箱) 与功能(自由度) 之间的平衡。
完全开放eval()或exec()给用户,无异于将服务器权限拱手相让,需要根据用户的技术水平和你期望的安全等级,选择不同的实现方案。
以下是几种主流的实现路径,从最安全、最受限到最灵活。
自定义领域特定语言(DSL) —— 最安全
这是最推荐的方式,尤其适合非技术用户,你定义一套简单、安全的语法规则,然后编写解析器将其翻译为PHP代码。
- 原理: 用户不写原生PHP,而是写一种你设计的简单指令集。
IF 订单金额 > 100 THEN 发送优惠券。 - 优点: 完全可控,不可能有安全漏洞,用户体验好(有语法提示)。
- 缺点: 开发工作量大,表达能力有限,无法做复杂逻辑。
实现思路:
- 定义语法: 比如支持
GET,SET,IF,THEN,LOOP, 以及一些预定义的变量({user.email},{order.total})。 - 词法分析 + 语法分析: 将用户输入的字符串拆分成Token(令牌),然后构建一个抽象语法树(AST)。
- 执行器: 遍历AST,执行对应的PHP函数。
示例: 用户输入脚本:
IF {订单金额} > 100 THEN SET {运费} = 0
系统解析后,变成逻辑:检查$order['total'],如果大于100,则设置$order['shipping'] = 0。
使用 PHP 沙箱扩展 —— 中等安全
如果用户确实需要写真正的PHP代码,那么必须使用沙箱,最成熟的两个方案是 php-sandbox(已停更)和 krakjoe/parallel(推荐)。
使用 krakjoe/parallel(需要安装扩展)
这是目前官方推荐的方向,它通过在独立的子线程(或者子进程)中运行用户代码,拥有独立的执行环境和内存空间。
- 优点: 真正隔离,
include/require不会污染主进程,可以限制CPU/内存执行时间。 - 缺点: 需要安装PHP扩展,环境配置稍复杂,不能直接共享变量(需要通过序列化传递)。
示例代码:
<?php
// 用户提交的脚本(已经过基本过滤)
$user_code = <<<'CODE'
$result = array_sum([1,2,3,4]);
// 用户无法访问主进程的 $db 等资源
CODE;
use parallel\Runtime;
try {
// 创建一个新的运行时环境
$runtime = new Runtime();
// 定义超时(可选,需要扩展支持)
$future = $runtime->run(function() use ($user_code) {
// 在子线程中执行
return eval($user_code); // 注意:这里只能 eval 用户代码的 '结果' 部分
});
// 等待结果,设置 5 秒超时
$result = $future->value(5000000);
echo "脚本结果: " . $result;
$runtime->close();
} catch (\parallel\Runtime\Error\Execution $e) {
echo "脚本执行错误: " . $e->getMessage();
} catch (\parallel\Runtime\Error\Timeout $e) {
echo "脚本执行超时!";
}
重要: 这个方案的核心思想是隔离,即使用户写了死循环 while(true){},也只会卡死子线程,主进程可以设置超时并杀掉它。
使用 WHMCS/Libraries/Hooks 或类似场景
很多商业CMS(如WordPress, WHMCS, Magento)通过钩子系统(Hooks/Actions/Filters) 实现用户自定义。
- 原理: 系统在关键流程(比如支付成功、用户注册)中预留一个“插槽”,用户可以在数据库或文件中注册一个回调函数。
- 执行: 系统遍历所有注册的回调,依次调用
call_user_func。 - 安全性: 通常信任用户(因为是管理员或开发者),所以直接
eval或include用户写的PHP文件。不推荐暴露给普通用户。
编译成其他安全语言 —— 高级安全
这是很多SaaS平台的终极方案,用户写一种声明式或限制性语言,然后编译成另一种东西。
- 例子: 用户写YAML、JSON或特定格式的数组,定义“....”,系统将此配置解析为PHP代码。
- 场景: 工作流引擎(如Laravel的Workflow)、商品促销规则。
优点: 非常安全,表达能力强于简单的DSL。 缺点: 用户需要学习一种新的表示方法,不如写代码直观。
纯 eval 并配合严格的 disable_functions —— 高风险
强烈不推荐,除非是给100%可信的顶级开发者用的调试工具。
- 做法:
// 极其危险! eval($user_input_code);
- 补救措施(但依然不够安全):
ini_set('disable_functions', 'exec,system,passthru,shell_exec,proc_open,...');- 使用
open_basedir限制文件系统访问。 - 设置
max_execution_time和memory_limit。 - 使用正则过滤
include,require,file_put_contents等危险函数。
风险: 总有绕过 disable_functions 的方法(例如利用 FFI 或 PCNTL),一旦被突破,服务器直接沦陷。
最佳实践建议
-
明确用户群体:
- 普通用户/运营人员: 选择 方案一(DSL),他们不需要懂编程,只需要知道“我设置了这个规则,就能得到那个结果”,这通常是产品经理和创业者最想要的。
- 技术型用户/开发者: 选择 方案二(Sandbox/Parallel),他们需要完整的编程能力来处理复杂计算。
-
防御性编程必须做:
- 输入长度限制: 防止提交百万行级脚本导致内存溢出。
- 执行超时: 使用
set_time_limit()或在parallel中设置Timeout。 - 资源限制: 限制内存
memory_limit。 - 日志记录: 记录谁在什么时间执行了什么脚本的全量内容(包括结果),方便审计和回滚。
- 速率限制: 单个用户每天或每分钟只能触发N次自定义脚本执行。
-
现代实现推荐:
- 如果你是用 Laravel,可以结合 Laravel Bouncer / Spatie Permissions 做权限,用 PHP-Parser (解析用户代码为AST,然后进行安全检查,再执行) 做更高级的静态分析。
- 最省心的纯PHP方案是使用
nikic/PHP-Parser配合php-master/php-sandbox,PHP-Parser可以解析用户代码,你可以写一个Visitor来遍历AST,自动禁止任何带有eval、exec、system等敏感节点的代码被执行,然后再运行通过检查的AST,这比parallel更可控,但性能稍差。
| 方案 | 安全性 | 灵活度 | 开发成本 | 适用场景 |
|---|---|---|---|---|
| 自定义DSL | 极高 | 低 | 高 | 电商规则、工作流、自动化 |
| Parallel沙箱 | 高 | 高 | 中 | 开发者工具、API动态处理 |
| 纯eval+严格限制 | 极低 | 极高 | 低 | 内部调试平台(极其不推荐公开) |
| 编译为安全格式 | 高 | 中 | 中 | 配置引擎、规则引擎 |
最终建议: 先从 方案一(DSL) 开始,用JSON或YAML定义规则,如果你发现用户真的需要写PHP代码,再大费周章地部署 方案二(Krakjoe/Parallel),切勿直接使用 eval()。