1. 从一次内部代码审计说起create_function()的“幽灵”几年前我在参与一个遗留系统的安全审计时遇到了一个非常典型的案例。那是一个用早期PHP版本开发的内部管理系统代码风格还停留在“能跑就行”的阶段。在审计一个用户反馈模块时我注意到了一段处理动态排序逻辑的代码它使用了create_function()来根据前端传入的参数动态生成一个用于usort()的比较函数。代码看起来人畜无害大概长这样$sortBy $_GET[sort] ?? id; $order $_GET[order] ?? asc; // 动态创建排序函数 $compareFunction create_function($a, $b, $valA $a[ . $sortBy . ]; $valB $b[ . $sortBy . ]; return $valA . ($order desc ? : ) . $valB ? 1 : -1; ); usort($dataArray, $compareFunction);从功能上看它完美地实现了动态排序。开发者可能还为自己的“聪明”做法感到得意。但问题就出在$sortBy和$order这两个变量是直接拼接进函数体字符串的。如果攻击者传入的sort参数是id];phpinfo();//呢拼接后的函数体就变成了$valA $a[id];phpinfo();//]; $valB $b[id];phpinfo();//]; return $valA $valB ? 1 : -1;//注释掉了后面的语法错误而phpinfo();就被成功注入并执行了。这就是create_function()最经典的代码注入漏洞。它本质上是在运行时用一个字符串拼凑出函数体然后调用eval()来执行这个字符串从而“创建”出一个函数。任何未经严格过滤的用户输入一旦被拼接进去就等于直接获得了执行任意PHP代码的能力。这个案例让我印象深刻不仅仅是因为漏洞本身更是因为它揭示了两个问题第一很多开发者并不清楚create_function()的内部实现原理及其危险性第二即使在现代PHP开发中由于历史代码、第三方老旧库或开发者习惯这个函数依然可能像幽灵一样潜伏在系统中。随着PHP 7.2将其标记为废弃并在PHP 8.0中彻底移除是时候彻底审视并清理这个“历史遗留问题”了。2.create_function()解剖为什么说它是“带毒的糖”要理解为什么create_function()如此危险我们必须深入它的本质。这个函数是在PHP 4时期引入的当时的PHP缺乏对匿名函数的原生支持。它的出现是为了满足一种动态创建函数逻辑的需求比如上面提到的动态回调。它的函数签名很简单create_function(string $args, string $code): string。第一个参数是函数参数的字符串比如$a, $b第二个参数是函数体的字符串。它返回一个唯一的字符串作为这个新生成函数的名称。关键在于它的内部实现。PHP官方并没有明确说明但通过社区分析和逆向工程普遍认为其内部逻辑伪代码类似这样function create_function($args, $code) { $function_name \0lambda_ . ($GLOBALS[__lambda_counter]); $full_code sprintf(function %s(%s) {%s}, $function_name, $args, $code); eval($full_code); return $function_name; }看明白了吗它实际上是在动态拼接一个完整的函数定义字符串然后通过eval()语言构造来执行这段字符串从而在全局作用域中定义这个函数。eval()是PHP中一个极其危险的函数它会将其字符串参数作为PHP代码来执行。这就把create_function()的所有风险都转移到了eval()上代码注入如果$args或$code参数的任何部分来源于不可信的用户输入如$_GET,$_POST,$_COOKIE并且没有经过正确的过滤和转义攻击者就可以注入任意PHP代码。作用域污染动态创建的函数被定义在全局作用域可能意外覆盖已有的函数或变量尤其是在包含include文件或循环创建时。性能低下每次调用create_function()都会进行一次eval()操作和一次函数定义。eval()本身开销就大因为它需要调用Zend引擎的编译器将字符串编译为opcode无法利用opcode缓存如OPcache在循环或高频调用场景下会成为性能瓶颈。难以调试和维护生成的函数名是类似\0lambda_1这样的不可打印字符在错误堆栈或调试器中可读性极差。代码逻辑以字符串形式存在IDE无法提供语法高亮、代码提示或静态分析大大降低了代码质量。所以create_function()是一颗“带毒的糖”。它用简单的语法糖封装了动态创建函数的能力但糖衣之下是eval()这个巨大的安全炸弹。在几乎所有情况下我们都有更好、更安全、更高效的替代方案。注意这里反复强调eval()是为了加深理解其原理。在实际开发和代码审计中直接搜索eval(和create_function(都是发现此类漏洞的关键词。3. 实战排查如何在项目中定位并清理“幽灵函数”既然知道了它的危害下一步就是在现有项目中把它找出来并清理掉。对于大型遗留项目这需要一套系统的方法。3.1 使用静态分析工具进行扫描这是最直接有效的方法。现代IDE和代码分析工具都能很好地识别create_function()。IDE全局搜索在PhpStorm、VSCode等IDE中使用全局搜索通常快捷键是CtrlShiftF或CmdShiftF搜索create_function。这是最基础的一步。命令行工具grep/ripgrep在项目根目录下运行grep -r create_function --include*.php .。ripgrep (rg)速度更快rg create_function --type php。PHP兼容性检查工具使用phpstan、phan或psalm这类静态分析工具。它们不仅能找到调用还能在你设置好PHP版本目标如phpVersion 80000后直接将其报告为错误或废弃警告。例如在phpstan.neon配置中设置级别为max它能捕获很多此类问题。专有安全扫描工具像RIPS、SonarQube配合PHP插件等SAST静态应用安全测试工具会专门将create_function()的使用标记为安全漏洞。3.2 人工审计的关键模式工具扫描会有漏报尤其是代码经过混淆或动态生成时。人工审计需要关注以下几种典型模式模式一动态回调生成。这是最常见场景常与usort()、array_filter()、array_map()、uasort()等需要回调函数的函数一起出现。重点检查回调函数体字符串中是否有变量拼接。// 高危模式 $callback create_function($item, return $item . $_GET[threshold] . ;); $result array_filter($array, $callback);模式二在模板或视图逻辑中。有些老旧模板引擎或自定义渲染逻辑会用create_function()来动态执行一些代码片段。模式三来自第三方库或框架。检查vendor/目录下的依赖。虽然现代主流库已弃用但一些陈旧或不活跃的库可能还在用。用composer why命令查看依赖关系如果某个包包含此函数且无法升级风险需要评估。3.3 清理与替换策略找到之后如何安全地替换核心原则是用匿名函数闭包替代。PHP 5.3引入的匿名函数是create_function()的完美继任者。替换案例对比场景1动态排序修复开头案例// 废弃的危险写法 $sortBy $_GET[sort] ?? id; $order $_GET[order] ?? asc; $compareFunction create_function($a, $b, $valA $a[ . $sortBy . ]; $valB $b[ . $sortBy . ]; return $valA . ($order desc ? : ) . $valB ? 1 : -1; ); usort($data, $compareFunction); // 安全的现代写法 $sortBy $_GET[sort] ?? id; $order $_GET[order] ?? asc; // 使用use关键字将外部变量安全地传入闭包作用域 usort($data, function($a, $b) use ($sortBy, $order) { $valA $a[$sortBy] ?? null; // 使用null合并运算符更健壮 $valB $b[$sortBy] ?? null; if ($order desc) { return $valB $valA; // 使用太空船运算符简化比较 } return $valA $valB; });关键改进消除了字符串拼接从根本上杜绝了代码注入。使用use关键字显式传递变量作用域清晰。使用了太空船运算符和 null 合并运算符??代码更简洁健壮。闭包可以直接作为参数传递无需中间变量。场景2简单的条件过滤// 旧写法 $filtered array_filter($users, create_function($u, return $u[age] 18;)); // 新写法 $filtered array_filter($users, function($u) { return ($u[age] ?? 0) 18; });场景3复杂的动态逻辑需要彻底重构有时create_function()被用来实现非常动态、复杂的逻辑直接替换为闭包可能不够。这时需要考虑重构将逻辑封装到独立的类方法或函数中通过可调用对象如[new Filter(), method]或filter_function来传递。// 难以直接替换的复杂案例假设逻辑来自配置 $logic getConfig(dynamic_logic); // 例如: return $x * 2 $y; $func create_function($x, $y, $logic); // 重构方案使用策略模式或解析器 class DynamicCalculator { private $strategy; public function __construct(string $expression) { // 安全地解析表达式例如使用专门的数学表达式库如 mathieuviossat/expression // 绝对不要用eval! $this-strategy $this-parseExpressionSafely($expression); } public function calculate($x, $y) { return $this-strategy-execute($x, $y); } private function parseExpressionSafely(string $expr): StrategyInterface { /* ... */ } } $calculator new DynamicCalculator($logic); $result $calculator-calculate($a, $b);3.4 处理无法立即替换的遗留代码在大型系统中可能有些地方的create_function()调用牵一发而动全身无法在短时间内安全重构。此时可以采取临时缓解措施输入过滤与验证如果无法移除必须确保拼接进函数体的所有变量都经过严格的白名单过滤或强类型转换。例如上面的排序字段$sortBy应该只允许预定义的字段名如[id, name, date]。$allowedSortFields [id, name, email]; $sortBy $_GET[sort] ?? id; if (!in_array($sortBy, $allowedSortFields)) { $sortBy id; } // ... 然后再用于危险的create_function这只是权宜之计封装与隔离将包含create_function()的代码块封装到一个独立的、输入经过严格校验的函数或类方法中限制其影响范围并添加清晰的废弃注释deprecated。升级PHP版本将环境升级到PHP 8.0以上。这样create_function()调用会直接导致致命错误迫使你修复它。这是最彻底的解决方案。4. 不仅仅是安全匿名函数带来的现代编程范式替换掉create_function()不仅仅是为了消除安全漏洞更是为了拥抱更现代、更强大的PHP编程范式。匿名函数闭包带来了诸多好处4.1 真正的词法作用域闭包可以捕获其创建时所在作用域的变量通过use关键字这使得它们非常灵活。你可以轻松创建配置化或状态化的回调。function createMultiplier($factor) { return function($number) use ($factor) { return $number * $factor; }; } $double createMultiplier(2); $triple createMultiplier(3); echo $double(5); // 10 echo $triple(5); // 15create_function()无法优雅地做到这一点通常需要借助全局变量或复杂的传参。4.2 作为一等公民匿名函数可以赋值给变量、作为参数传递、作为返回值与其他类型的值一样处理。这使得高阶函数和函数式编程风格在PHP中成为可能。$operations [ add fn($a, $b) $a $b, // PHP 7.4 箭头函数更简洁 sub fn($a, $b) $a - $b, ]; $result $operations[$_GET[op]] ?? $operations[add]; echo $result(10, 5);4.3 更好的性能和可调试性闭包由Zend引擎原生支持可以被OPcache完全缓存。它们出现在错误堆栈跟踪中时会显示为{closure}并附带文件名和行号极大方便了调试。Fatal error: Uncaught TypeError: {closure}(): Argument #1 must be of type int, string given in /path/to/file.php:15相比之下create_function()生成的函数错误信息几乎无法定位问题。4.4 箭头函数PHP 7.4对于简单的单表达式闭包PHP 7.4引入了箭头函数语法极其简洁并且自动捕获父作用域变量。// 旧世界 $squared create_function($x, return $x * $x;); // 新世界 (PHP 7.4) $squared fn($x) $x * $x; // 自动捕获外部变量 $y 10; $adder fn($x) $x $y; // $y 自动从父作用域捕获5. 深入防御从create_function到广义代码注入防护清理create_function()是修复一个具体漏洞但更重要的是建立防止此类代码注入的纵深防御体系。create_function()只是eval()家族的一个代表类似的危险函数/结构还有eval()万恶之源直接执行字符串代码。assert()在PHP 7.0之前字符串参数会被执行。PHP 7.0后改为评估表达式并返回布尔值但依然不应用于处理用户输入。preg_replace()的/e修饰符已废弃在替换字符串中执行PHP代码。create_function()本文主角内部使用eval()。array_filter(),array_map(),usort()等当与create_function()或包含用户输入的字符串回调一起使用时。shell_exec(),system(),exec(),passthru()系统命令注入是另一类但同等严重的注入漏洞。反序列化漏洞unserialize()通过精心构造的序列化字符串触发对象属性中的魔法方法如__wakeup,__destruct间接执行代码。构建防御策略输入验证与过滤白名单优于黑名单对所有用户输入进行严格的、基于预期类型的验证。对于像排序字段、操作类型这样的参数使用白名单是最有效的方法。// 坏黑名单永远防不住所有情况 // 好白名单只允许已知的安全值 $allowedActions [view, edit, delete]; $action $_GET[action] ?? view; if (!in_array($action, $allowedActions, true)) { throw new InvalidArgumentException(Invalid action specified.); }输出转义根据上下文如果必须将用户输入嵌入到另一种语言如SQL、HTML、JavaScript中必须在对应的上下文中进行正确的转义。SQL使用参数化查询PDO预处理语句或MySQLi预处理语句永远不要拼接SQL字符串。HTML使用htmlspecialchars($string, ENT_QUOTES | ENT_HTML5, UTF-8)。Shell命令使用escapeshellarg()或escapeshellcmd()但最好避免直接调用shell。JSON使用json_encode()。在PHP代码字符串中本应避免如果极端情况下必须动态生成PHP代码应极力避免需确保用户输入部分被正确处理例如通过var_export($input, true)将其转换为合法的PHP字面量字符串但这非常复杂且危险不推荐。使用安全的数据交换格式当需要将逻辑或数据从存储中动态加载时使用安全的、非代码的格式。例如用JSON、YAML配置文件来定义行为然后用PHP代码解析这些配置而不是存储PHP代码字符串。如果需要动态表达式考虑使用专门的、沙箱化的表达式语言库。代码审查与自动化扫描将搜索eval(、create_function(、assert(等危险函数作为代码审查的必查项。在CI/CD流水线中集成静态应用安全测试SAST工具自动拦截含有此类模式的代码合并。最小权限原则运行PHP的Web服务器进程如www-data, nginx应具有尽可能少的系统权限。禁用危险的PHP函数可以在php.ini中通过disable_functions指令实现disable_functions eval,assert,create_function,system,exec,shell_exec,passthru,proc_open,...这能从根本上防止这些函数被调用但要注意可能影响某些合法功能。保持更新及时升级PHP版本。每个新版本都会修复安全漏洞并移除或废弃不安全的特性。停留在老旧版本本身就是最大的安全风险。回过头看create_function()的废弃和移除是PHP语言在安全性和现代性上迈出的重要一步。作为开发者我们的任务不仅是修复眼前这个具体的函数调用更是要理解其背后的安全模型缺陷并将这种安全意识贯彻到整个开发流程中从源头构建更健壮、更安全的应用程序。