PHP 安全实践
本章将介绍 PHP Web 开发中的安全最佳实践。
输入验证
永远不要信任用户输入
<?php
// 验证整数
$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
if ($id === false || $id < 1) {
die('无效的 ID');
}
// 验证邮箱
$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
if ($email === false) {
die('无效的邮箱');
}
// 验证 URL
$url = filter_input(INPUT_POST, 'url', FILTER_VALIDATE_URL);
// 验证布尔值
$remember = filter_input(INPUT_POST, 'remember', FILTER_VALIDATE_BOOLEAN);
// 自定义验证
$username = $_POST['username'] ?? '';
if (!preg_match('/^[a-zA-Z][a-zA-Z0-9_]{2,19}$/', $username)) {
die('用户名格式无效');
}
?>
白名单验证
<?php
// 使用白名单验证
$allowedActions = ['create', 'update', 'delete'];
$action = $_POST['action'] ?? '';
if (!in_array($action, $allowedActions)) {
die('无效的操作');
}
// 验证枚举值
$allowedStatus = ['pending', 'active', 'inactive'];
$status = $_POST['status'] ?? 'pending';
if (!in_array($status, $allowedStatus)) {
$status = 'pending'; // 使用默认值
}
?>
XSS 防护
输出转义
<?php
// HTML 上下文
echo htmlspecialchars($user_input, ENT_QUOTES, 'UTF-8');
// JavaScript 上下文
echo json_encode($user_input, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP);
// URL 上下文
echo urlencode($user_input);
// CSS 上下文(避免)
// 不要将用户输入放入 CSS
// 辅助函数
function e($string) {
return htmlspecialchars($string ?? '', ENT_QUOTES, 'UTF-8');
}
// 使用
echo "<p>" . e($username) . "</p>";
?>
Content Security Policy
<?php
// 设置 CSP 头
header("Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'");
// 更严格的 CSP
header("Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'self'");
?>
SQL 注入防护
使用预处理语句
<?php
// 正确:使用预处理语句
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$_GET['id']]);
// 错误:直接拼接 SQL
// $sql = "SELECT * FROM users WHERE id = " . $_GET['id']; // 危险!
// 命名参数
$stmt = $pdo->prepare("SELECT * FROM users WHERE email = :email");
$stmt->execute(['email' => $_POST['email']]);
?>
CSRF 防护
Token 验证
<?php
session_start();
// 生成 Token
function csrf_token() {
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
// 验证 Token
function verify_csrf_token($token) {
return isset($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token);
}
// 表单中
// <input type="hidden" name="csrf_token" value="<?php echo csrf_token(); ?>">
// 验证
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!verify_csrf_token($_POST['csrf_token'] ?? '')) {
http_response_code(403);
die('CSRF 验证失败');
}
// 处理表单...
}
?>
SameSite Cookie
<?php
// 设置 SameSite 属性
session_set_cookie_params([
'lifetime' => 3600,
'path' => '/',
'domain' => 'example.com',
'secure' => true,
'httponly' => true,
'samesite' => 'Strict' // 或 'Lax'
]);
session_start();
?>
密码安全
密码哈希
<?php
// 创建密码哈希
$hash = password_hash('user_password', PASSWORD_DEFAULT);
// 指定算法和选项
$hash = password_hash('user_password', PASSWORD_BCRYPT, [
'cost' => 12 // 默认 10,越高越安全但越慢
]);
// 验证密码
if (password_verify($password, $hash)) {
echo "密码正确";
}
// 检查是否需要重新哈希
if (password_needs_rehash($hash, PASSWORD_DEFAULT)) {
$newHash = password_hash($password, PASSWORD_DEFAULT);
// 更新数据库中的哈希
}
?>
密码策略
<?php
function validatePassword($password) {
$errors = [];
if (strlen($password) < 8) {
$errors[] = '密码至少 8 个字符';
}
if (!preg_match('/[A-Z]/', $password)) {
$errors[] = '密码必须包含大写字母';
}
if (!preg_match('/[a-z]/', $password)) {
$errors[] = '密码必须包含小写字母';
}
if (!preg_match('/[0-9]/', $password)) {
$errors[] = '密码必须包含数字';
}
if (!preg_match('/[^A-Za-z0-9]/', $password)) {
$errors[] = '密码必须包含特殊字符';
}
// 检查常见密码
$commonPasswords = ['password', '123456', 'qwerty'];
if (in_array(strtolower($password), $commonPasswords)) {
$errors[] = '密码太常见';
}
return $errors;
}
?>
会话安全
<?php
// 安全的会话配置
ini_set('session.cookie_httponly', 1); // 仅 HTTP
ini_set('session.cookie_secure', 1); // 仅 HTTPS
ini_set('session.cookie_samesite', 'Strict');
ini_set('session.use_strict_mode', 1); // 拒绝未初始化的 Session ID
session_start();
// 登录后重新生成 Session ID
session_regenerate_id(true);
// 验证 User-Agent
if (!isset($_SESSION['user_agent'])) {
$_SESSION['user_agent'] = $_SERVER['HTTP_USER_AGENT'];
} elseif ($_SESSION['user_agent'] !== $_SERVER['HTTP_USER_AGENT']) {
session_destroy();
die('会话验证失败');
}
// 设置会话超时
$timeout = 1800; // 30 分钟
if (isset($_SESSION['last_activity']) && time() - $_SESSION['last_activity'] > $timeout) {
session_destroy();
die('会话已过期');
}
$_SESSION['last_activity'] = time();
?>
文件上传安全
<?php
function validateUpload($file) {
$errors = [];
// 检查错误码
if ($file['error'] !== UPLOAD_ERR_OK) {
$errors[] = '上传失败';
return $errors;
}
// 检查文件大小
$maxSize = 2 * 1024 * 1024; // 2MB
if ($file['size'] > $maxSize) {
$errors[] = '文件太大';
}
// 检查文件类型(通过内容,而非扩展名)
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($file['tmp_name']);
if (!in_array($mimeType, $allowedTypes)) {
$errors[] = '不支持的文件类型';
}
// 检查文件扩展名
$allowedExts = ['jpg', 'jpeg', 'png', 'gif'];
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!in_array($ext, $allowedExts)) {
$errors[] = '不支持的文件扩展名';
}
// 检查文件名(避免路径遍历)
$filename = basename($file['name']);
if ($filename !== $file['name']) {
$errors[] = '无效的文件名';
}
return $errors;
}
// 安全保存文件
function saveUpload($file, $destination) {
// 生成安全的文件名
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
$newName = bin2hex(random_bytes(16)) . '.' . $ext;
$filepath = $destination . '/' . $newName;
// 移动文件
if (move_uploaded_file($file['tmp_name'], $filepath)) {
return $newName;
}
return false;
}
?>
安全头设置
<?php
// 防止 MIME 类型嗅探
header('X-Content-Type-Options: nosniff');
// 防止点击劫持
header('X-Frame-Options: DENY');
// XSS 保护
header('X-XSS-Protection: 1; mode=block');
// HSTS(仅 HTTPS)
header('Strict-Transport-Security: max-age=31536000; includeSubDomains');
// 引用策略
header('Referrer-Policy: strict-origin-when-cross-origin');
// 权限策略
header("Permissions-Policy: geolocation=(), microphone=(), camera=()");
?>
错误处理
<?php
// 生产环境配置
ini_set('display_errors', 0);
ini_set('log_errors', 1);
ini_set('error_log', '/var/log/php_errors.log');
// 自定义错误处理器
set_error_handler(function ($severity, $message, $file, $line) {
error_log("Error: $message in $file on line $line");
return true;
});
// 自定义异常处理器
set_exception_handler(function (Throwable $e) {
error_log($e->getMessage() . "\n" . $e->getTraceAsString());
// 生产环境不显示详细错误
if (getenv('APP_ENV') === 'production') {
http_response_code(500);
echo "服务器内部错误";
} else {
throw $e;
}
});
?>
小结
本章我们学习了:
- 输入验证和过滤
- XSS 防护
- SQL 注入防护
- CSRF 防护
- 密码安全
- 会话安全
- 文件上传安全
- 安全头设置
下一章我们将学习现代 PHP 特性。