PHP 安全实践
Web 安全不是可选项,而是编码的基本功。PHP 8.x 提供了大量内置安全工具,但它们需要开发者主动使用。本页覆盖 PHP 应用中最常见的安全威胁和对应防御手段。
密码存储
永远不要明文存储密码。使用 password_hash() 和 password_verify():
// ✅ 存储密码
$hash = password_hash($password, PASSWORD_DEFAULT);
// PASSWORD_DEFAULT 当前使用 bcrypt,未来会自动升级算法
$stmt = $pdo->prepare('INSERT INTO users (email, password) VALUES (?, ?)');
$stmt->execute([$email, $hash]);
// ✅ 验证密码
$stmt = $pdo->prepare('SELECT password FROM users WHERE email = ?');
$stmt->execute([$email]);
$user = $stmt->fetch();
if ($user && password_verify($inputPassword, $user['password'])) {
// 登录成功
}
// ✅ 检查是否需要升级哈希(算法更新后)
if (password_needs_rehash($user['password'], PASSWORD_DEFAULT)) {
$newHash = password_hash($inputPassword, PASSWORD_DEFAULT);
// 更新数据库
}
为什么不用 MD5/SHA1?
- MD5/SHA1 速度太快,GPU 每秒可暴力破解数十亿次
- 没有 salt,相同密码产生相同哈希
password_hash()自动加 salt,使用 bcrypt/Argon2 等慢哈希算法
SQL 注入防护
SQL 注入至今仍是 PHP 应用中最严重的安全威胁。核心原则:绝不拼接用户输入到 SQL 语句中。
// ❌ 危险:直接拼接
$sql = "SELECT * FROM users WHERE id = " . $_GET['id'];
// 攻击者传入: id=1 OR 1=1
// ✅ 安全:预处理语句
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = ?');
$stmt->execute([$_GET['id']]);
$user = $stmt->fetch();
// ✅ 安全:命名占位符
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email AND status = :status');
$stmt->execute([':email' => $email, ':status' => $status]);
PDO 安全配置
$pdo = new PDO($dsn, $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // 异常模式
PDO::ATTR_EMULATE_PREPARES => false, // 真正的预处理
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // 关联数组
]);
XSS 防护
跨站脚本攻击(XSS)通过在页面中注入恶意脚本窃取用户信息。输出编码是唯一可靠的防御。
// ❌ 危险:直接输出用户输入
echo $_GET['name'];
// ✅ 安全:htmlspecialchars 转义
echo htmlspecialchars($_GET['name'], ENT_QUOTES, 'UTF-8');
// ✅ 在模板中统一处理
function e(string $value): string {
return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
}
echo e($_GET['name']);
上下文感知编码
| 输出位置 | 编码方式 |
|---|---|
| HTML 正文 | htmlspecialchars($v, ENT_QUOTES, 'UTF-8') |
| HTML 属性 | htmlspecialchars($v, ENT_QUOTES, 'UTF-8') |
| JavaScript | json_encode($v) |
| URL 参数 | urlencode($v) |
| CSS | css_escape($v) |
CSRF 防护
跨站请求伪造(CSRF)诱导已登录用户执行非预期操作。防御方法是为每个表单嵌入唯一 Token。
// 生成 Token(页面加载时)
session_start();
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
// 表单中嵌入
echo '<input type="hidden" name="csrf_token" value="' . e($_SESSION['csrf_token']) . '">';
// 提交时验证
if ($_POST['csrf_token'] !== $_SESSION['csrf_token']) {
die('CSRF token mismatch');
}
输入验证
// ✅ 始终验证,绝不信任用户输入
$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
if ($email === false) {
// 无效邮箱
}
$age = filter_input(INPUT_POST, 'age', FILTER_VALIDATE_INT, [
'options' => ['min_range' => 0, 'max_range' => 150]
);
// ✅ 白名单验证
$allowed = ['jpg', 'png', 'gif'];
$ext = strtolower(pathinfo($_FILES['avatar']['name'], PATHINFO_EXTENSION));
if (!in_array($ext, $allowed)) {
die('不允许的文件类型');
}
Session 安全
session_start();
// 1. 启动时生成新 Session ID(防固定会话攻击)
session_regenerate_id(true);
// 2. 设置安全 Cookie 参数
ini_set('session.cookie_httponly', 1); // JS 无法读取
ini_set('session.cookie_secure', 1); // 仅 HTTPS
ini_set('session.cookie_samesite', 'Lax'); // 防 CSRF
// 3. 设置 Session 过期时间
ini_set('session.gc_maxlifetime', 1800); // 30 分钟
// 4. 销毁 Session
session_unset();
session_destroy();
文件上传安全
// ✅ 安全的文件上传处理
function secureUpload(array $file, string $uploadDir, array $allowedExts): string {
// 1. 检查错误
if ($file['error'] !== UPLOAD_ERR_OK) {
throw new RuntimeException('上传失败');
}
// 2. 限制文件大小(2MB)
if ($file['size'] > 2 * 1024 * 1024) {
throw new RuntimeException('文件过大');
}
// 3. 验证 MIME 类型(不要只依赖扩展名)
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($file['tmp_name']);
// 4. 白名单检查
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!in_array($ext, $allowedExts)) {
throw new RuntimeException('不允许的文件类型');
}
// 5. 生成随机文件名(防路径穿越和覆盖)
$newName = bin2hex(random_bytes(16)) . '.' . $ext;
$dest = rtrim($uploadDir, '/') . '/' . $newName;
// 6. 移动文件
if (!move_uploaded_file($file['tmp_name'], $dest)) {
throw new RuntimeException('移动失败');
}
return $newName;
}
安全配置清单
- error_reporting: 生产环境关闭错误输出,记录到日志
- display_errors:
Off(生产环境) - expose_php:
Off(隐藏 PHP 版本头) - allow_url_fopen:
Off(防止远程文件包含) - open_basedir: 限制 PHP 可访问的目录
- disable_functions: 禁用危险函数(
exec,system,passthru等)
下一步
- PHP 数据库 — PDO 预处理语句详解
- PHP 表单 — 表单验证与安全处理
- PHP 8.x 新特性 — 语言层面的安全增强