exec()和shell_exec()输入过滤不能仅靠trim(),须用白名单校验、escapeshellarg()包裹参数、realpath()限制路径、disable_functions禁用危险函数,并监控返回码与输出长度。

exec() 和 shell_exec() 的输入过滤不能只靠 trim()
很多人以为对 $_POST['cmd'] 调用 trim() 就算“清理过了”,其实这完全没用——攻击者输入 ls / | cat /etc/passwd 或 id; whoami,trim() 一个字符都不会删。真正有效的过滤必须基于「白名单」或「结构验证」。
建议做法:
- 若命令固定(如只允许查磁盘空间),直接硬编码命令,用
escapeshellarg()包裹参数部分,例如:df -h ' . escapeshellarg($path) . ' - 若需支持多个命令,用数组白名单校验:
in_array($cmd, ['df', 'uptime', 'free'], true),禁止任何带空格、分号、管道符的输入 - 禁用所有 shell 元字符:用
preg_replace('/[;&|`$(){}\[\]\n\r]/', '', $input)清洗,但注意这不能替代白名单,仅作兜底
escapeshellarg() 不等于安全,它只保单个参数不破壳
escapeshellarg() 的作用很窄:把字符串变成一个 shell 可安全引用的参数,比如把 hello'world 变成 'hello'''world'。但它不防命令拼接漏洞,也不防路径遍历或命令注入。
典型错误写法:
立即学习“PHP免费学习笔记(深入)”;
$cmd = 'ls ' . escapeshellarg($_GET['dir']); // ❌ dir=/.ssh%00 仍可能绕过 system($cmd);
更稳妥的做法:
- 先用
realpath()+str_starts_with()限制目录范围,例如只允许在/var/www/data/下操作 - 再用
escapeshellarg()包裹最终路径,而非原始输入 - 避免拼接多个用户可控字段,如
ls ' . escapeshellarg($a) . ' | grep ' . escapeshellarg($b)—— 这种结构本身已违反最小信任原则
disable_functions 配置必须包含 passthru、exec、shell_exec、system
光靠代码层过滤远远不够。一旦某处漏掉校验,或引入第三方库执行了未审查的命令,exec() 等函数就是后门入口。最直接的防线是 PHP 层禁用。
在 php.ini 中设置:
disable_functions = exec,passthru,shell_exec,system,proc_open,popen,pcntl_exec
注意两点:
- 必须重启 PHP-FPM 或 Apache 才生效,
ini_set()在运行时无法启用该限制 - 某些 SaaS 环境(如共享主机)可能已全局禁用,但你仍要检查
get_cfg_var('disable_functions')确认,别假设它一定开着
返回码和输出长度都要监控,异常即中止
命令执行后,仅看输出内容是否“看起来正常”是危险的。攻击者可能让命令静默成功(如 touch /tmp/.pwned),或故意超长输出触发缓冲区问题。
实操建议:
- 始终检查
$return_var,非0值应记录并拒绝后续逻辑 - 用
mb_strlen($output) < 10240限制输出长度,防止内存耗尽或日志刷屏 - 对输出做基础敏感词扫描(如
/root:.*:0:0:/、PRIVATE KEY),命中则立即丢弃并告警
真正的难点不在“怎么写过滤”,而在于“哪里用了命令执行”——项目里可能藏在日志归档脚本、图片缩略图生成、PDF 导出回调这些不起眼的地方。定期 grep -r 'exec|shell_exec|system|passthru' ./ 比写一百行过滤逻辑都管用。



















