PHP后端在接收任何外部输入时,第一道防线不是过滤函数,而是明确的数据边界。绝大多数注入和篡改都源于开发者对“这个参数应该是什么”没有做严格定义。比如用户ID应该是一个正整数,订单号应该是固定长度的数字串,手机号应该是11位数字。如果不在入口处校验这些规则,后面再多的过滤都可能被绕过。最直接的做法是建立一个统一的输入校验层,对GET、POST、COOKIE、HTTP Header中的所有参数,按照白名单方式定义类型、长度、格式和取值范围。白名单的意思是:只允许符合规则的数据进入业务逻辑,不符合的直接拒绝,而不是尝试去“清洗”那些本来就该被丢弃的数据。

以常见的用户ID参数为例,很多代码直接写$id = $_GET['id'];然后拼进SQL语句。正确的做法是先判断这个ID是否由纯数字组成,并且长度在合理范围内。如果业务上ID是从1开始自增的,那么小于1的值也没有必要进入查询。这种校验不是安全加固,而是最基本的程序逻辑。把校验逻辑封装成一个独立的函数或类,例如checkInt($value, $min, $max),在控制器入口统一调用,可以避免每个业务方法里重复写判断,也减少遗漏。

SQL注入的核心问题在于数据和指令没有分离。使用PDO预处理语句是目前最可靠的防注入手段。预处理语句让数据库先解析SQL结构,再把参数作为纯数据绑定进去,参数中的任何SQL关键字都不会改变语句结构。但要注意,预处理只能保护数据值的位置,不能保护表名、字段名、ORDER BY方向、LIMIT数值等结构部分。如果业务需要动态拼接表名或字段名,必须使用白名单映射。例如排序字段只允许id、create_time、price这几个值,排序方向只允许ASC和DESC,其他输入一律使用默认值。

$allowedColumns = ['id', 'create_time', 'price'];
$column = in_array($_GET['sort'], $allowedColumns, true) ? $_GET['sort'] : 'id';
$direction = strtoupper($_GET['dir'] ?? 'DESC') === 'ASC' ? 'ASC' : 'DESC';
$stmt = $pdo->prepare("SELECT * FROM orders ORDER BY {$column} {$direction} LIMIT :limit");
$stmt->bindValue(':limit', (int)$_GET['limit'], PDO::PARAM_INT);
$stmt->execute();

上面的代码中,列名和排序方向经过白名单校验后才拼接,LIMIT的值通过预处理绑定并且强制转换为整数。即使攻击者传入limit=1;DROP TABLE orders,这个值也只会被当作整数1处理,不会执行额外语句。PDO默认会模拟预处理,在某些情况下仍可能被绕过,建议在连接时设置PDO::ATTR_EMULATE_PREPARES为false,让数据库驱动真正执行原生预处理。

防篡改的核心场景包括表单数据被中途修改、接口参数被重放、页面关键内容被注入脚本。对于表单提交,除了服务端校验,还需要考虑数据完整性。比如一个电商下单接口,商品价格不应该由前端传过来,而应该由服务端根据商品ID从数据库重新查询。前端传来的价格只用于展示,服务端计算总价时一律以数据库价格为准。用户ID、商品ID、数量这些关键参数在提交后,服务端要再次验证其合法性和业务状态,比如商品是否上架、库存是否充足、用户是否有购买权限。

对于需要防止参数被篡改的接口,可以使用签名机制。客户端和服务端约定一个密钥,对请求参数按照固定顺序拼接后生成HMAC签名,服务端收到请求后用同样的方式计算签名并比对。签名不一致说明参数在传输过程中被修改过。签名机制不能防止重放攻击,所以还需要加上时间戳和随机数。时间戳用于限制请求的有效期,比如超过5分钟的请求直接丢弃。随机数用于防止同一请求在有效期内被重复提交,服务端可以缓存近期用过的随机数,发现重复则拒绝。这套机制适合开放API或前后端分离项目中安全性要求较高的接口。

XSS攻击的防护重点在于输出编码,而不是输入过滤。很多开发者习惯在数据入库前用htmlspecialchars过滤,这样做有两个问题:一是数据可能被用于多种场景,比如JSON输出、纯文本邮件、HTML属性,不同场景需要的编码方式不同,入库前编码会导致数据被重复编码或编码不足;二是如果业务逻辑需要读取原始数据做处理,编码后的数据会干扰处理结果。正确的做法是数据原样入库,在输出到HTML页面时根据上下文使用对应的编码函数。输出到HTML标签内容时用htmlspecialchars($value, ENT_QUOTES, 'UTF-8'),输出到HTML属性时同样处理,输出到JavaScript变量时用json_encode,输出到URL参数时用urlencode。

文件上传是另一个高风险入口。防止文件上传漏洞的核心是:不信任客户端提供的任何文件信息,包括文件名、文件类型、文件大小。服务端必须重新检测文件的真实MIME类型,可以通过finfo扩展读取文件内容来判断,而不是依赖$_FILES['file']['type'],因为这个值来自客户端HTTP头,可以随意伪造。对于图片上传,可以用getimagesize验证文件确实是图片格式。文件保存时不要使用用户提供的原始文件名,而是用服务端生成的随机文件名加上服务端确定的后缀。文件保存目录要禁止执行PHP脚本,可以在该目录下放置一个.htaccess文件,内容为php_flag engine off,或者将文件存储在Web根目录之外,通过专门的读取脚本来输出文件内容。

$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($_FILES['file']['tmp_name']);
$allowedMime = ['image/jpeg', 'image/png', 'image/gif'];
if (!in_array($mime, $allowedMime, true)) {
    die('文件类型不允许');
}
$ext = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'image/gif' => 'gif'][$mime];
$newName = bin2hex(random_bytes(16)) . '.' . $ext;
move_uploaded_file($_FILES['file']['tmp_name'], '/var/www/uploads/' . $newName);

CSRF攻击利用的是浏览器自动携带Cookie的特性,让用户在不知情的情况下向已登录的网站发送伪造请求。防护手段是在表单中嵌入一个随机的token,服务端验证这个token是否与当前会话中保存的一致。对于AJAX请求,可以把token放在请求头中。对于敏感操作,比如修改密码、转账、删除数据,除了token验证,还可以增加二次确认,比如要求输入当前密码或验证码。SameSite Cookie属性也能在一定程度上缓解CSRF,设置Cookie时加上SameSite=Lax或Strict,可以限制跨站请求携带Cookie,但这不是唯一防线,仍然需要配合token使用。

会话安全直接影响防篡改能力。PHP默认的会话机制存在一些风险,比如会话ID可能被固定、会话文件可能被读取。登录成功后应该使用session_regenerate_id(true)重新生成会话ID,防止会话固定攻击。会话Cookie应设置HttpOnly属性,防止JavaScript读取会话ID,设置Secure属性确保只在HTTPS下传输。会话数据中不要存放敏感信息,比如密码明文、支付密钥。对于长时间不活动的会话,应该设置过期时间并自动销毁。如果使用Redis或Memcached存储会话,要确保存储服务本身不暴露在公网,并且设置了访问密码。

错误信息泄露是经常被忽视的问题。生产环境中如果开启display_errors,PHP会把文件路径、数据库结构、函数调用栈等信息直接输出给用户,这些信息对攻击者非常有价值。正确做法是在php.ini中设置display_errors=Off,log_errors=On,把错误记录到日志文件中,日志文件放在Web根目录之外。对于用户可见的错误信息,只显示通用的提示,比如“操作失败,请稍后重试”,不要把具体的SQL错误、文件路径、类名暴露出来。自定义错误处理函数可以统一控制错误输出格式,同时记录完整的错误上下文用于排查。

权限控制是防篡改的最后一道关卡。很多系统只在前端隐藏了没有权限的按钮,但后端接口没有做权限校验,攻击者直接调用接口就能操作。每个需要权限的接口都必须在服务端验证当前用户是否具备操作权限,不能依赖前端隐藏。对于水平权限,比如用户只能查看自己的订单,需要在查询条件中强制加上用户ID过滤,不能只靠前端传过来的ID。对于垂直权限,比如管理员功能,需要验证用户角色。权限校验应该放在业务逻辑执行之前,并且尽量靠近控制器层,避免在业务代码深处才做判断导致遗漏。

日志记录和监控是发现篡改行为的重要手段。关键操作比如登录、修改密码、支付、删除数据,应该记录操作者、操作时间、操作内容、IP地址、User-Agent。日志要包含足够的信息用于事后追溯,但不能记录密码、token等敏感数据。对于异常行为,比如短时间内大量失败登录、同一账号在不同地区同时登录、请求参数中出现明显的注入特征,应该触发告警。定期分析日志可以发现潜在的攻击尝试,及时修补漏洞。

PHP配置层面的加固同样重要。在php.ini中关闭allow_url_fopen和allow_url_include,防止远程文件包含漏洞。设置open_basedir限制PHP只能访问指定目录,即使有文件包含漏洞也无法读取系统其他目录的文件。禁用危险的函数,比如exec、system、passthru、shell_exec、popen、proc_open,如果业务确实需要,可以针对特定目录或特定脚本开启。使用最新版本的PHP,及时更新安全补丁,旧版本中存在的已知漏洞很容易被自动化工具利用。

防注入防篡改不是靠某一个函数或某一个配置就能解决的,需要从输入校验、数据处理、输出编码、权限控制、日志监控多个层面同时设防。每一层都可能被绕过,但多层防护叠加后,攻击成本会大幅提高。开发者在写每一行代码时都应该问自己:这个数据从哪里来,它是否可信,它会被用在哪里,如果它被恶意构造会产生什么后果。带着这些问题去写代码,比记住一百个过滤函数更有效。