PHP 安全开发指南:SQL注入 / XSS / CSRF / 文件上传漏洞防护

zjlxcf
zjlxcf 正式会员正式会员认证极客认证极客
发布于 2026-09-29 09:46 ·3 浏览 ·0 回复

照着这篇做完,你能把 PHP 项目里最常见的四类漏洞——SQL 注入、XSS、CSRF、文件上传——用可落地的代码一次性堵住。

这四类漏洞占了 PHP 老项目安全问题的绝大多数,而且防护手段都不复杂,难的是每次都记得做。下面就按「先看怎么错、再看怎么改」的顺序一个个过。

第一步:SQL 注入——永远不要拼字符串

错误写法,看一眼就知道问题在哪:

$id = $_GET['id'];
$sql = "SELECT * FROM posts WHERE id = $id";   // 注入点

正确做法是用 PDO 预处理,把数据和语句彻底分开:

$stmt = $pdo->prepare('SELECT * FROM posts WHERE id = ?');
$stmt->execute([$_GET['id']]);
$row = $stmt->fetch();

两个容易忽略的细节:

  • 关闭模拟预处理,否则预处理只是字符串转义的伪装。连接时加上 `$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);`
  • LIKE 查询要自己转义通配符。用户输入的 `%` 和 `_` 在 LIKE 里是元字符,得先 `addcslashes($kw, '%_\\')` 再拼进参数。
  • 表名、字段名、ORDER BY 的字段无法参数化,这类只能用白名单:
$allow = ['id', 'created_at', 'views'];
$order = in_array($_GET['order'] ?? '', $allow, true) ? $_GET['order'] : 'id';

注意:`mysql_real_escape_string`、`addslashes` 这类转义函数不能替代预处理,字符集设错时照样被绕过。新项目直接用 PDO 或 mysqli 的预处理,别再考虑转义方案。

第二步:XSS——所有输出都要转义

XSS 的本质是「用户输入被当成 HTML 执行了」。核心原则一句话:入库不管,出库必转。

echo htmlspecialchars($row['title'], ENT_QUOTES, 'UTF-8');

`ENT_QUOTES` 会把单引号和双引号一起转义,这样放进 `value="..."` 这类属性里也安全。第三个参数 `'UTF-8'` 不能省,编码不匹配时中文可能被截断成半个字符,反而制造新问题。

不同上下文要求不同:

输出位置处理方式
HTML 正文/属性`htmlspecialchars`
`<script>` 里的 JS 变量`json_encode($v, JSON_HEX_TAG\
URL 参数`urlencode()`

如果站点要支持富文本(发帖带格式),就不能整体转义,得走标签白名单过滤,只放行 `p / br / strong / a / img / code` 等,其余一律剥掉,并强制过滤 `on*` 事件属性和 `javascript:` 协议。

注意:过滤和转义是两件事,别只做其中一件。存入数据库时保留原文(方便编辑),渲染时再按上下文转义或过滤。

第三步:CSRF——表单必须带 token

CSRF 是「借你的登录态发请求」,所以防护靠的是一个攻击者猜不到的随机值。

生成(放在 session 里):

if (empty($_SESSION['csrf'])) {
    $_SESSION['csrf'] = bin2hex(random_bytes(32));
}

表单里带上:

<input type="hidden" name="csrf" value="<?= $_SESSION['csrf'] ?>">

提交时校验,用 `hash_equals` 做定时安全比较:

if (!hash_equals($_SESSION['csrf'], $_POST['csrf'] ?? '')) {
    exit('页面已过期,请刷新后重试');
}

再加一层 Cookie 属性:`session_set_cookie_params(['samesite' => 'Lax', 'httponly' => true, 'secure' => true]);`,让浏览器在跨站请求时就不带 Cookie。

注意:token 校验失败最常见的原因不是被攻击,而是页面停留太久或登录态变了。解决方法是在提交层做一次自动重试——拉一个新 token 再发一遍,同时后台把站点地址(含 https:// 和主域名)配对,避免 www 和裸域混用导致会话丢失。

第四步:文件上传——白名单 + 二次校验

上传是拿 shell 的重灾区,四道关卡缺一不可:

1. 扩展名白名单,不是黑名单:

$allow = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
$ext = strtolower(pathinfo($_FILES['file']['name'], PATHINFO_EXTENSION));
if (!in_array($ext, $allow, true)) { exit('文件类型不允许'); }

2. 不信任 `$_FILES['type']`,那是客户端传来的,随手就能改。

3. 图片二次校验,确认它真的是图片而不是改名后的 PHP 文件:

if (!@getimagesize($_FILES['file']['tmp_name'])) { exit('不是有效图片'); }

4. 重命名 + 目录不可执行。用 `bin2hex(random_bytes(8)) . '.' . $ext` 做新文件名,去掉原始文件名里的一切可疑字符;同时确保上传目录没有 PHP 执行权限(Nginx 里对该目录 `location` 禁止 `\.php$`)。

注意:上传失败的排查顺序是——① 扩展名白名单是否包含该格式;② PHP 的 `upload_max_filesize` 和 `post_max_size` 是否大于文件体积(面板默认常是 2M,手机照片轻松超过,建议调到 30M 以上);③ 上传目录是否可写。很多"上传失败"根本不是代码问题,而是这三处配置。

小结

  • SQL:一律预处理,关掉模拟模式;表名字段名走白名单。
  • XSS:输出即转义,按上下文选函数;富文本用标签白名单过滤。
  • CSRF:session 存随机 token + 表单隐藏字段 + `hash_equals` 校验 + SameSite Cookie,并给用户留一条自动重试的路。
  • 上传:扩展名白名单、忽略 `type`、`getimagesize` 二次校验、随机重命名、目录禁执行。
  • 密码用 `password_hash()`(bcrypt),别再用 md5/sha1;报错信息不要回显给用户,写日志就行。

四条规则都不长,把它们做成公共函数或封装层,比每次靠记性靠谱得多。

本文转载自 Clara轻量论坛系统 - 轻量级 PHP 论坛系统,原文地址:https://www.leleweb.cn/thread-636.html
转载请注明出处,版权归原作者所有。
他们都看过 1 人浏览过
CLARA轻量论坛系统

全部回复 0

还没有回复,来抢沙发~