PHP应用中几乎所有的用户交互都依赖参数传递,GET请求的查询字符串、POST提交的表单、Cookie、请求头,都是参数的来源。参数本身只是数据,但一旦服务端不加校验就直接拿去拼接SQL、执行命令、包含文件,它就变成了攻击的载体。本文围绕PHP接收参数这一环节,梳理常见的漏洞类型和对应的防范方法,帮助开发者在数据入口处建立第一道防线。

一、PHP接收参数的常见漏洞类型
首先要明确一点:PHP的$_GET、$_POST、$_REQUEST等超全局数组本身并不存在漏洞,它们只是把请求数据原样装进数组。真正的问题出在开发者如何使用这些数据。下面几种是出现频率最高的漏洞场景。
第一类是SQL注入。当开发者把用户输入直接拼进SQL语句时,攻击者可以构造特殊字符串改变SQL的语义。例如$sql = "SELECT * FROM users WHERE id = " . $_GET['id'];这样的写法,一旦id传入1 OR 1=1,就会导致全表数据泄露;传入精心构造的UNION语句甚至可以直接拖库。这类漏洞危害极大,是OWASP排行榜上的常客。
第二类是XSS跨站脚本攻击。参数中的HTML或JavaScript代码未经转义就输出到页面上,攻击者可以注入恶意脚本窃取其他用户的Cookie或会话信息。比如评论内容里插入<script>标签,如果输出时不做htmlspecialchars处理,每个浏览该页面的用户都会中招。
第三类是命令注入与文件包含漏洞。如果参数被传入exec、system、shell_exec等函数,攻击者可以通过分号、管道符拼接额外命令;如果参数被用作include或require的路径,攻击者可以借助../穿越目录,甚至在开启远程包含的情况下执行远程代码。此外还有CSRF、上传漏洞、变量覆盖等问题,也都与参数处理不当有关。
二、SQL注入的防范:预处理语句是根本解法
防范SQL注入最有效的方式是使用PDO或MySQLi的预处理语句,也就是参数化查询。它的原理是把SQL语句的结构与数据彻底分离:先把带有占位符的SQL模板发给数据库完成编译,再把实际数据作为参数传进去。数据永远只被当作数据,不会被解释成SQL指令的一部分,注入自然无从谈起。
// 使用PDO预处理语句,安全地接收参数
$pdo = new PDO('mysql:host=localhost;dbname=test;charset=utf8mb4', 'user', 'pass');
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); // 关闭模拟预处理
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id AND status = :status');
$stmt->execute([':id' => $_GET['id'], ':status' => 1]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);有几个细节需要注意。首先,建议设置PDO::ATTR_EMULATE_PREPARES为false,让预处理真正由数据库完成,而不是PHP层面的模拟。其次,占位符只能绑定值,不能绑定表名、列名等结构部分。如果这些部分确实需要动态化(比如排序字段),必须用白名单校验,例如只允许id、created_at等预定义值通过,绝不能直接拼接用户输入。
有些人习惯用addslashes或手动str_replace转义来防注入,这种做法并不可靠。不同数据库、不同编码下转义规则不同,宽字节注入等手法可以绕过简单转义。预处理语句之外,还应遵循最小权限原则,数据库账号只授予必要的权限,即使被注入也能把损失控制在有限范围内。
三、输入校验与过滤:把脏数据挡在门外
预处理解决了SQL注入,但参数还可能被用在其他地方,所以入口处的校验和过滤同样重要。核心思路是:明确每个参数的预期类型和格式,拒绝一切不符合预期的输入,也就是所谓的白名单思想。
对数字型参数,直接做类型强制转换,例如$id = (int)$_GET['id'];,转换后不管传入什么内容都只会得到一个整数。对字符串参数,使用PHP内置的过滤扩展filter_var配合验证过滤器,可以校验邮箱、URL、IP等常见格式:
// 校验邮箱格式
$email = filter_var($_POST['email'] ?? '', FILTER_VALIDATE_EMAIL);
if ($email === false) {
exit('邮箱格式不正确');
}
// 过滤掉字符串中的非法标签
$comment = filter_var($_POST['comment'] ?? '', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
// 严格校验数字范围
$page = filter_var($_GET['page'] ?? 1, FILTER_VALIDATE_INT,
['options' => ['min_range' => 1, 'max_range' => 1000]]);对于枚举类参数,例如状态值、排序字段,用in_array做白名单校验是最稳妥的方案。对于文件路径相关的参数,要用basename去掉目录部分,并配合realpath确认最终路径没有越出允许的范围。需要强调的是,PHP7.2之后filter_var校验失败返回false,校验成功返回原值,判断时务必使用全等比较,避免把合法的0误判为失败。
四、输出转义与其他场景的加固
安全上有句老话:输入做过滤,输出做转义。参数最终输出到HTML页面时,必须根据输出上下文进行转义。输出到HTML正文用htmlspecialchars($str, ENT_QUOTES, 'UTF-8'),输出到JavaScript中用json_encode,输出到URL中用urlencode。不要指望在入口处过滤一次就万事大吉,同一份数据可能输出到不同上下文,只有输出点转义才能确保安全。
// 输出到HTML时转义,防止XSS echo htmlspecialchars($user['nickname'], ENT_QUOTES, 'UTF-8'); // 输出到JS上下文时使用json_encode ?><script> var config = <?php echo json_encode(['uid' => $uid], JSON_HEX_TAG); ?>; </script>
其他场景也需要针对性加固:涉及写操作的表单要配合Token验证防CSRF;文件上传要校验扩展名、MIME类型和文件内容,重命名存储;尽量避免调用系统命令,迫不得已时用escapeshellarg包裹每个参数;动态包含文件时用白名单映射,而不是直接拼接路径。在php.ini层面,建议关闭register_globals遗留风险相关的写法,开启open_basedir限制文件访问范围,并保持PHP版本及时更新。
五、建立统一的参数接收流程
零散的防护容易遗漏,更好的做法是在项目中建立统一的参数接收层。可以封装一个输入类,集中完成默认值设置、类型转换、格式校验和长度限制,业务代码只从这一层取数据,不再直接触碰超全局数组。这样即使某个参数规则变更,也只需修改一处。
class Input
{
// 统一获取并校验整数参数
public static function int(string $key, int $default = 0, int $min = 0, int $max = PHP_INT_MAX): int
{
$value = $_REQUEST[$key] ?? $default;
$value = (int)$value;
if ($value < $min || $value > $max) {
return $default;
}
return $value;
}
// 统一获取字符串参数,限制长度并做白名单过滤
public static function string(string $key, int $maxLen = 200, array $allowedTags = []): string
{
$value = trim((string)($_REQUEST[$key] ?? ''));
if (mb_strlen($value) > $maxLen) {
$value = mb_substr($value, 0, $maxLen);
}
return strip_tags($value, $allowedTags);
}
}
// 业务代码中统一调用
$uid = Input::int('uid', 0, 1, 999999999);
$keyword = Input::string('keyword', 50);总结来看,PHP接收参数的安全问题并非语言本身的缺陷,而是使用方式的问题。记住三条主线:SQL一律走预处理语句,输入一律做白名单校验,输出一律按上下文转义。再辅以最小权限的数据库配置、统一封装的参数接收层和定期的代码审计,就能有效抵御绝大多数针对参数的攻击。安全是一场持续的工作,把好参数这道入口关,整个应用的风险面会大幅缩小。