引言
安全不是「上线前补一遍」,而是贯穿每个输入、每处输出的工程习惯。PHP 应用最常见的五大致命漏洞——SQL 注入、XSS、CSRF、文件上传、会话劫持——各有明确解法。本文按攻击面逐条给「攻击方式 → 正确防护 → Laravel 内置支持」,并对照 OWASP Top 10,让你从「会写功能」进阶到「敢上生产」。
前置:/php-mysql-database/(预处理防注入)、/php-laravel-internals/(中间件)。
目录
- 1. 攻击面地图与 OWASP Top 10
- 2. SQL 注入:预处理是第一道闸
- 3. XSS:输出转义的双向战线
- 4. CSRF:跨站请求伪造
- 5. 会话安全:固定与劫持
- 6. 文件上传漏洞
- 7. SSRF 与开放重定向
- 8. 敏感数据与密钥管理
- 9. Laravel 内置防护清单
- 10. 安全自检速查表
- 延伸阅读
1. 攻击面地图与 OWASP Top 10
攻击面 = 一切「不可信输入」的入口:
浏览器输入(GET/POST/Cookie)→ 应用
文件上传 / 导入 → 存储
第三方 API / Webhook → 应用
数据库 / 缓存 → 应用
OWASP Top 10(2021)对照本专题:
| OWASP 项 | 本专题章节 |
|---|---|
| A1 访问控制失效 | 8(授权检查) |
| A3 注入 | 2(SQL 注入) |
| A3 敏感数据暴露 | 8 |
| A4 不安全设计 | 6/7(上传/SSRF) |
| A7 身份认证失效 | 5(会话) |
| A8 软件和数据完整性失效 | 依赖锁定 |
| A9 日志与监控 | 自检(留痕) |
心智:先画攻击面,再逐面加固——漏一个入口等于白加十道锁。
2. SQL 注入:预处理是第一道闸
攻击:把 SQL 片段拼进查询,改变原意:
// 致命反例:字符串拼接
$sql = "SELECT * FROM users WHERE id = " . $_GET['id'];
// 输入:1 OR 1=1 → 全表泄露
// 输入:1; DROP TABLE users; -- → 删表
正确:PDO 预处理 + 占位符(见 /php-mysql-database/):
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = ?');
$stmt->execute([$_GET['id']]); // 参数作为数据,不参与 SQL 解析
附加防线:
// 白名单校验
$allowed = ['id', 'name', 'email'];
if (!in_array($orderBy, $allowed, true)) { $orderBy = 'id'; }
// 强制类型(数字型字段)
$id = (int) $_GET['id'];
注入防护层次:① 预处理(必须)→ ② 类型强制 → ③ 白名单 → ④ 最小权限数据库账号。
3. XSS:输出转义的双向战线
存储型 XSS:用户输入 <script> 存库,别人浏览时执行;反射型:直接拼进响应。
// 致命反例:未转义输出
echo "你好,{$_GET['name']}";
// 输入:<script>alert(document.cookie)</script>
正确防护:
// 输出到 HTML 上下文 → htmlspecialchars 转义
echo '你好,' . htmlspecialchars($name, ENT_QUOTES, 'UTF-8');
// 输出到 JavaScript 上下文 → 用 json_encode 而非拼接
echo '<script>const name = ' . json_encode($name, JSON_HEX_TAG | JSON_HEX_AMP) . ';</script>';
| 上下文 | 转义方式 |
|---|---|
| HTML 文本/属性 | htmlspecialchars($s, ENT_QUOTES) |
| JavaScript | json_encode + HEX 旗标 |
| URL | urlencode / rawurlencode |
| CSS | 尽量避免用户进 CSS |
Laravel 帮了忙:Blade 的 {{ }} 自动转义({!! !!} 才裸输出)。
铁律:输入校验防「坏数据入库」,输出转义防「坏数据执行」——两者都要,双向战线。
4. CSRF:跨站请求伪造
攻击:受害者已登录,攻击者在别的站点诱导其浏览器向你的站点发请求:
<!-- 攻击站点上的 img -->
<img src="https://bank.com/transfer?to=hacker&amount=1000">
Cookie 会自动带上 → 请求看似来自受害者。
防护:CSRF 令牌(每次表单带随机 token,服务端校验):
// 生成并校验(Laravel 已内置)
// 视图
<form method="POST">
@csrf
<input name="amount">
</form>
// 请求校验(中间件 VerifyCsrfToken)
// 非表单请求:X-CSRF-TOKEN 头
要点:
| 场景 | 做法 |
|---|---|
| 表单 POST | 带 token 字段 |
| API(无 Cookie 认证) | 可免 CSRF(token 认证本身防) |
| SameSite Cookie | SameSite=Lax 兜底 |
| 换绑/提现等敏感 | 二次确认 + token |
记忆:CSRF 防的是「借刀杀人」——令牌就是验明「这把刀是你本人举的」。
5. 会话安全:固定与劫持
会话固定攻击:攻击者先设好 PHPSESSID 发给受害者登录 → 攻击者用同一 SID 冒充。
会话劫持:窃取 SID(XSS 偷 Cookie、网络嗅探)。
防护清单:
// 会话 Cookie 配置(php.ini 或 session_set_cookie_params)
session_set_cookie_params([
'httponly' => true, // JS 读不到 Cookie(防 XSS 偷)
'secure' => true, // 仅 HTTPS 传输
'samesite' => 'Lax', // 防跨站携带
]);
// 登录成功后必须重新生成 SID(防固定攻击)
session_regenerate_id(true);
// 超时与最大生命周期
ini_set('session.gc_maxlifetime', 1800);
| 措施 | 防什么 |
|---|---|
| HttpOnly Cookie | XSS 偷 SID |
| Secure | 明文网络嗅探 |
| session_regenerate_id 登录后 | 会话固定 |
| 超时过期 | 闲置被利用 |
| 设备/指纹校验 | 异常登录(进阶) |
铁律:登录成功 = SID 换新的时刻——这一步忘了,其余全白搭。
6. 文件上传漏洞
上传是 PHP 应用高危区:直接放 web 可执行目录 = RCE。
// 危险反例
move_uploaded_file($_FILES['file']['tmp_name'], 'uploads/' . $_FILES['file']['name']);
// 正确:白名单扩展名 + 随机名 + 非执行目录
$allowed = ['jpg', 'png', 'gif', 'webp'];
$ext = strtolower(pathinfo($_FILES['file']['name'], PATHINFO_EXTENSION));
if (!in_array($ext, $allowed, true)) {
throw new RuntimeException('不支持的文件类型');
}
$newName = bin2hex(random_bytes(16)) . '.' . $ext; // 随机名
move_uploaded_file($tmp, storage_path('uploads/' . $newName)); // 放 web 目录外
完整清单:
| 检查 | 说明 |
|---|---|
| 扩展名白名单 | 别用黑名单 |
| MIME 校验 | 只作辅助(可伪造) |
| 内容头检测 | getimagesize 验图 |
| 随机文件名 | 防路径穿越/覆盖 |
| 存储位置 | web 目录外,经路由服务 |
| 大小限制 | upload_max_filesize |
| 病毒扫描 | 生产必备 |
| 图片二次处理 | 去 exif/重新编码 |
7. SSRF 与开放重定向
SSRF(服务端请求伪造):用户控制 URL,服务器代打内网:
// 危险:curl 直接请求用户给的 URL → 可访问 127.0.0.1 内部服务
$res = file_get_contents($_GET['url']);
// 防护:白名单域名 + 禁止内网地址
$allowed = ['https://api.example.com', 'https://cdn.example.com'];
if (!in_array(parse_url($url, PHP_URL_HOST), $allowed, true)) {
throw new RuntimeException('URL 不在白名单');
}
// 或解析后验证 IP 不是内网段
开放重定向:header('Location: ' . $_GET['next']) 被用于钓鱼。
防护:只重定向到本站路径(相对路径),或用白名单域名表。
SSRF 关键:「服务器能访问的内网」是攻击者的跳板——URL 必须白名单,IP 必须排除内网段(127.x、10.x、172.16-31、192.168)。
8. 敏感数据与密钥管理
密钥泄露是终极翻车——.env 进 Git、日志打密码、响应返 token。
防护清单:
// 1. 密钥不进代码/仓库
// .env 加入 .gitignore,CI 用环境变量注入
// 2. 敏感数据脱敏
$logger->info('用户注册', ['email' => maskEmail($email)]); // a***@x.com
// 3. 密码哈希(绝不 MD5/SHA 裸存)
$hash = password_hash($raw, PASSWORD_BCRYPT, ['cost' => 12]);
password_verify($input, $hash);
// 4. 传输加密
// 全站 HTTPS + HSTS 头
// 5. 最小权限
// 数据库账号只给需要的库/表权限
| 数据 | 处理 |
|---|---|
| 密码 | bcrypt/argon2 哈希 |
| token/密钥 | 环境变量,日志脱敏 |
| 个人数据 | 加密存储 + 访问审计 |
| 响应 | 不返回明文密码/token 冗余字段 |
记忆:密钥的敌人是「不小心」——环境变量注入、日志脱敏、响应裁剪,三管齐下。
9. Laravel 内置防护清单
用框架就免写一半:
| Laravel 内置 | 防护 |
|---|---|
VerifyCsrfToken 中间件 | CSRF 默认开启 |
{{ }} 模板转义 | XSS 默认转义 |
DB:: / Eloquent 参数绑定 | SQL 注入默认防护 |
password_hash 辅助 | 密码哈希 |
Auth 门面 | 认证与会话 |
RateLimiter | 暴力破解限流 |
.env 不进库 | 密钥管理约定 |
Storage 私有盘 | 上传隔离 |
仍需自己做的:业务级授权(policy)、上传白名单、第三方回调校验、SSRF 白名单、审计日志。
// 示例:授权(Policy)——资源级访问控制
class OrderPolicy {
public function view(User $user, Order $order): bool {
return $user->id === $order->user_id;
}
}
10. 安全自检速查表
| 检查项 | 通过标准 |
|---|---|
| SQL 注入 | 全部预处理占位符,无拼接 |
| XSS | 输出全转义,无裸 echo 用户数据 |
| CSRF | 表单带 token,SameSite=Lax |
| 会话 | HttpOnly + Secure + 登录后换 SID |
| 上传 | 白名单 + 随机名 + web 外目录 |
| SSRF | URL 白名单 + 内网 IP 排除 |
| 密钥 | .env 不进库、日志脱敏 |
| 密码 | bcrypt/argon2 |
| HTTPS | 全站 + HSTS |
| 限流 | 登录/接口有速率限制 |
| 依赖 | composer.lock 锁定 + 漏洞扫描 |
一句话记忆:注入走预处理、XSS 走转义、CSRF 走令牌、会话登录换 SID、上传白名单随机名、SSRF 白名单、密钥进环境变量——七道闸过了,生产才敢开。
延伸阅读
- /php-mysql-database/ — 预处理与注入防护落地
- /php-api-design-rest/ — API 的认证与速率限制
- /php-laravel-internals/ — 中间件管道的安全接入点
- [[security]] — 系统安全的纵深防御
- [[network]] — 传输层与 TLS
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。