PHP接收表单数据通常使用$_GET、$_POST和$_REQUEST这三个超全局变量,但很多开发者容易忽略一个关键问题:所有来自客户端的数据都必须视为不可信输入。直接从$_POST里取值并拼接到SQL语句或输出到页面,等于把后门交给了攻击者。本文会从基础获取方式讲到注入攻击原理,再给出可落地的安全过滤与验证方案,帮助你在表单处理环节建立完整防线。

一、PHP获取表单数据的常见方式
在PHP中,表单数据根据HTTP提交方法的不同,会被自动解析到对应的超全局数组中。使用method="post"提交的数据存放在$_POST,使用GET方式传参或查询字符串则存放在$_GET。另外还有一个$_REQUEST,它会合并GET、POST和COOKIE数据,但具体合并顺序受PHP配置变量request_order和variables_order影响,默认通常是GP(GET优先于POST),这意味着如果GET和POST中存在同名字段,$_REQUEST取到的值可能不是你预期的那一个。
最简单也最常见的写法是直接读取数组键名:
// 获取POST提交的用户名 $username = $_POST['username']; // 获取GET参数中的分页页码 $page = $_GET['page']; // 获取REQUEST混合值(不推荐,来源不明确) $id = $_REQUEST['id'];
这种写法的好处是短,但缺陷同样明显:如果键名不存在,PHP会抛出Undefined array key警告,在较新的PHP版本中还会形成错误日志。更重要的是,它完全没有过滤,拿到什么就是什么。比较稳妥的初始做法是先判断键是否存在,并提供一个默认值。例如使用空合并运算符??可以避免警告:$username = $_POST['username'] ?? '';。这样至少保证变量恒有定义,但安全防护远不止于此。
另一类需求是处理数组型表单字段,比如多选框<input type="checkbox" name="hobby[]">,提交后$_POST['hobby']会是一个数组。处理数组时同样需要遍历每个元素做过滤,不能用简单的trim或htmlspecialchars直接作用于数组,否则会得到字符串Array或触发类型错误。针对数组输入,推荐使用array_map配合过滤函数逐个处理。
二、直接使用用户输入会带来哪些典型风险
最大的风险当属SQL注入。如果开发者把用户输入直接拼进SQL语句,攻击者可以通过构造闭合引号和OR条件,绕过登录验证甚至读取整张数据表。下面这段代码就是典型的反例:
$mysqli = new mysqli("localhost", "user", "pass", "db");
$name = $_POST['name'];
// 危险:直接拼接SQL,存在SQL注入
$sql = "SELECT * FROM users WHERE name = '$name'";
$result = $mysqli->query($sql);
假如攻击者在用户名框中输入' OR '1'='1' -- ,最终SQL会变成SELECT * FROM users WHERE name = '' OR '1'='1' --',条件永远成立,登录校验形同虚设。更严重的情况下,攻击者还能利用UNION查询、报错注入、时间盲注等手段拖走数据库中的敏感数据。
除了SQL注入,XSS跨站脚本攻击也极为常见。当用户提交的评论内容未经转义直接输出到页面时,攻击者可以插入<script>alert(document.cookie)</script>,一旦其他用户访问该页面,恶意脚本就会在受害者浏览器中执行,从而窃取Cookie、伪造请求或植入木马。还有文件包含漏洞,如果用户输入被用于include或require路径,可能触发本地文件包含或远程文件包含。命令注入则出现在system、exec等函数中,用户输入被拼接到系统命令里时,可以用分号、管道符追加额外命令。这些风险的共同根源都是“信任了外部输入”。
三、PHP安全接收用户输入的核心实践
安全处理用户输入的第一原则是:在入口处做验证,在使用前做转义。验证是判断数据是否符合预期格式,转义是针对不同输出场景处理特殊字符。两者不能互相替代。例如对邮箱地址,可以用filter_var配合FILTER_VALIDATE_EMAIL验证,只有验证通过才继续后续逻辑;对整数参数,直接使用(int)强制转换或filter_var配合FILTER_VALIDATE_INT,能简单有效地消除大部分注入可能。
下面是一组常见的过滤与验证代码:
// 去除首尾空格
$username = trim($_POST['username'] ?? '');
// 检查长度
if (mb_strlen($username) < 3 || mb_strlen($username) > 30) {
die('用户名长度必须在3到30个字符之间');
}
// 整数参数强制转换
$id = (int)($_GET['id'] ?? 0);
if ($id <= 0) {
die('无效的ID');
}
// 邮箱验证
$email = filter_var($_POST['email'], FILTER_VALIDATE_EMAIL);
if (!$email) {
die('邮箱格式不正确');
}
// 过滤URL
$url = filter_var($_POST['url'], FILTER_VALIDATE_URL);
对于需要存入数据库的字符串,务必使用预处理语句。预处理语句将SQL结构与数据分离,从机制上杜绝了SQL注入。以mysqli为例:
$mysqli = new mysqli("localhost", "user", "pass", "db");
$stmt = $mysqli->prepare("SELECT * FROM users WHERE email = ?");
$stmt->bind_param("s", $email);
$stmt->execute();
$result = $stmt->get_result();
如果使用PDO,可以把错误模式设为异常,并同样使用占位符。无论哪种驱动,都不要再手动拼接用户数据。对于输出到页面的内容,则要使用htmlspecialchars转义,避免XSS。例如echo htmlspecialchars($username, ENT_QUOTES, 'UTF-8');。如果需要保留部分HTML格式,可以考虑使用HTML Purifier这类白名单过滤库,而不是简单使用strip_tags,因为strip_tags很难处理属性中的事件脚本。
此外,表单处理还需要考虑CSRF跨站请求伪造。攻击者可以诱导受害者点击一个自动提交的表单,以受害者的身份执行操作。有效防护方法是在表单中加入随机token,服务端验证该token与session中保存的是否一致。生成token可以简单使用bin2hex(random_bytes(32)),存入session后渲染到隐藏字段,提交时用hash_equals做比较,避免时序攻击。
四、一个完整的表单处理示例
下面给出一个包含CSRF防护、输入验证和预处理语句的登录示例。前端表单部分如下,注意这里所有HTML标签在代码块中已经转义,实际使用时按正常HTML书写即可:
<form method="post" action="login.php">
<input type="text" name="username" placeholder="用户名">
<input type="password" name="password" placeholder="密码">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($_SESSION['csrf_token']); ?>">
<button type="submit">登录</button>
</form>
后端login.php可以这样组织:
session_start();
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
exit('非法请求');
}
// 验证CSRF Token
if (!isset($_POST['csrf_token']) || !hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
exit('CSRF验证失败');
}
// 过滤和验证输入
$username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? '';
if ($username === '' || $password === '') {
exit('用户名和密码不能为空');
}
// 模拟从数据库查询用户(请替换为预处理语句)
$pdo = new PDO("mysql:host=localhost;dbname=test;charset=utf8mb4", "user", "pass");
$stmt = $pdo->prepare("SELECT id, password_hash FROM users WHERE username = ?");
$stmt->execute([$username]);
$user = $stmt->fetch();
if ($user && password_verify($password, $user['password_hash'])) {
$_SESSION['user_id'] = $user['id'];
echo '登录成功';
} else {
echo '用户名或密码错误';
}
这段代码中,CSRF验证放在业务逻辑之前,任何非POST请求直接拒绝。用户名经过trim去掉首尾空白,密码不进行trim以免改变用户真实输入,但长度校验应在更外层完成。数据库查询使用PDO预处理语句。密码验证使用password_verify,绝不直接存储或比较明文密码。
实际项目中,建议再把过滤逻辑抽离成公共函数或使用成熟的验证库,比如Respect Validation或Symfony Validator。同时开启PHP的error_reporting和日志记录,但生产环境不要向用户显示具体错误,而是记录到服务器日志。任何外部输入都必须经过验证后才能进入业务流程,这是最基本的安全底线。
最后提醒一点,安全不是一个函数就能解决的事,它是一整套编码习惯。从获取表单数据的瞬间开始,就要保持“所有输入都是恶意”的假设,这样写出的代码才更稳健。