从危险函数、污点传播到静态扫描与 CI 门禁,梳理 PHP 项目可落地的漏洞检测方法、常见误报与修复路径。
PHP 项目的安全问题,往往不是因为没有做安全测试,而是因为检测动作没有进入日常研发流程。等到上线前再做人工审计,风险暴露面和修复成本都已经很高。
真正可落地的漏洞检测,需要把代码规则、依赖检查、运行时观测和修复闭环连接起来。本文从 PHP 的常见危险入口出发,整理一套适合中小型研发团队的工程化扫描方案。
一、先识别高风险入口
PHP 的漏洞通常来自外部输入进入危险操作。代码审计时,可以优先关注以下几类 sink:
- 命令执行:
system、exec、shell_exec、passthru、popen、proc_open、反引号。 - 代码执行:
eval、assert、动态函数调用、动态类实例化。 - 文件操作:用户可控路径进入
include、require、file_get_contents、unlink、rename。 - 反序列化:用户输入进入
unserialize,尤其是存在可利用魔术方法的类。 - 数据库:SQL 字符串拼接、未参数化的排序字段、动态表名和动态列名。
- 网络请求:用户可控 URL 进入服务端请求,形成 SSRF;上传文件未校验类型、内容和存储位置。
这些函数不一定有漏洞。判断关键不是函数名,而是外部输入是否可以不受约束地到达这些位置。
二、用污点分析替代关键词扫描
直接搜索 eval 或 exec 会产生大量误报。更有效的方式是跟踪 source 到 sink 的传播路径:
- source:
$_GET、$_POST、$_COOKIE、$_FILES、请求头、消息队列和第三方接口返回。 - sink:命令执行、文件包含、SQL 执行、反序列化和服务端请求等危险操作。
- sanitizer:类型转换、白名单校验、参数化查询、路径规范化、转义与权限检查。
例如下面的代码虽然使用了变量,但风险并不同:
// 高风险:用户输入直接进入命令
$file = $_GET['file'];
system('tail -n 100 ' . $file);
// 相对可控:固定命令,参数单独转义
$file = $_GET['file'];
system('tail -n 100 -- ' . escapeshellarg($file));
第二种写法仍需要路径白名单和权限校验,但它至少消除了命令拼接带来的直接注入面。扫描规则应该理解这种差异,而不是只判断是否出现 system。
三、建立分层检测流程
1. 本地与提交前检查
在编辑器或 Git hooks 中执行轻量规则,尽早阻止明显问题。可检查危险函数新增、调试代码、硬编码密钥、未处理的文件上传和敏感配置。
2. 静态分析与依赖审计
使用 PHPStan、Psalm 或基于 AST 的自定义规则检查类型、调用链和高风险模式;使用 Composer Audit 检查已知依赖漏洞。对需要跨文件跟踪的场景,可以引入 Semgrep 或自建污点分析规则。
3. CI 门禁
把扫描放进每次合并请求:高危规则直接失败,中危规则生成待办,所有结果保留基线。没有基线的扫描很快会因历史告警而失效,因此要区分“本次新增”和“存量问题”。
4. 运行时观测
日志、WAF 和异常监控可以发现静态规则覆盖不到的攻击尝试。重点记录来源 IP、请求路径、参数摘要、命中规则和处置结果,同时避免把敏感数据原样写入日志。
四、控制误报,才能让扫描持续运行
漏洞检测最大的成本通常不是发现漏洞,而是团队不愿意处理误报。规则需要结合框架和业务上下文:ThinkPHP、Laravel、Symfony 的输入封装方式不同,ORM 与原生 SQL 的风险边界也不同。
- 对确认安全的路径提供带责任人和到期时间的豁免,而不是永久关闭规则。
- 把规则分为阻断、告警和观察三级,避免所有问题都阻塞发布。
- 对每一类问题给出修复示例和验证方式,降低开发人员的处理成本。
- 定期回看误报规则,调整 sanitizer 和框架识别逻辑。
五、修复要落到具体边界
命令执行优先使用固定命令和白名单参数,避免拼接用户输入;SQL 查询使用参数绑定,动态排序字段必须映射到有限集合;文件操作先规范化路径,再确认目标位于允许目录;反序列化尽量改为 JSON,并使用签名、白名单类型和最小权限。
漏洞检测的目标不是让扫描报告变成零,而是让高风险路径在进入生产环境之前被发现、被解释、被修复,并最终沉淀为团队可以复用的工程规则。