PHP 安全编码完全指南
输入验证与过滤
1 输入验证原则
// ❌ 不安全的做法
$id = $_GET['id'];
$query = "SELECT * FROM users WHERE id = $id";
// ✅ 安全的做法
$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
if ($id === false || $id === null) {
throw new InvalidArgumentException('Invalid ID');
}
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$id]);
2 常用过滤函数
// 验证邮箱
$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
// 验证URL
$url = filter_input(INPUT_POST, 'url', FILTER_VALIDATE_URL);
// 验证IP地址
$ip = filter_input(INPUT_SERVER, 'REMOTE_ADDR', FILTER_VALIDATE_IP);
// 自定义验证
function validateInput($data, $type) {
switch ($type) {
case 'name':
return preg_match('/^[a-zA-Z\s\-]{2,50}$/', $data);
case 'phone':
return preg_match('/^\+?[0-9\-]{8,15}$/', $data);
// 更多自定义规则...
}
}
SQL注入防护
1 使用PDO预处理语句
// ✅ PDO预处理(推荐)
$pdo = new PDO('mysql:host=localhost;dbname=test', $user, $pass);
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// 使用命名参数
$stmt = $pdo->prepare("SELECT * FROM users WHERE email = :email AND status = :status");
$stmt->execute([
':email' => $_POST['email'],
':status' => 1
]);
$user = $stmt->fetch();
// 使用位置参数
$stmt = $pdo->prepare("INSERT INTO users (name, email) VALUES (?, ?)");
$stmt->execute([$_POST['name'], $_POST['email']]);
2 使用MySQLi
// ✅ MySQLi预处理
$mysqli = new mysqli('localhost', 'user', 'pass', 'db');
$stmt = $mysqli->prepare("SELECT * FROM users WHERE id = ?");
$stmt->bind_param('i', $id);
$stmt->execute();
$result = $stmt->get_result();
XSS攻击防护
1 输出编码
// ❌ 不安全:直接输出用户输入
echo $_POST['username'];
// ✅ 安全:HTML实体编码
echo htmlspecialchars($_POST['username'], ENT_QUOTES, 'UTF-8');
// ✅ 更安全的输出函数
function escape_output($data) {
return htmlspecialchars($data ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
2 使用模板引擎的转义功能
// Twig模板引擎
$twig->render('user.html', [
'name' => $_POST['name'] // Twig自动转义 {{ name }}
]);
// Blade模板引擎 (Laravel)
echo $name; // Blade {{ $name }} 自动转义
echo {!! $content !!}; // 不转义(需确保$content安全)
CSRF防护
1 生成和验证Token
class CSRF {
public static function generateToken() {
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
public static function validateToken($token) {
return isset($_SESSION['csrf_token'])
&& hash_equals($_SESSION['csrf_token'], $token);
}
}
// 在表单中使用
$token = CSRF::generateToken();
?>
<form method="POST">
<input type="hidden" name="csrf_token" value="<?= $token ?>">
<!-- 其他表单字段 -->
</form>
<?php
// 验证POST请求
if (!CSRF::validateToken($_POST['csrf_token'] ?? '')) {
http_response_code(403);
die('CSRF token validation failed');
}
文件上传安全
1 安全文件上传处理
class FileUploadHandler {
private $allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
private $maxSize = 2 * 1024 * 1024; // 2MB
public function upload($file) {
// 验证文件大小
if ($file['size'] > $this->maxSize) {
throw new Exception('File too large');
}
// 验证MIME类型(使用finfo,不信任客户端)
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($file['tmp_name']);
if (!in_array($mimeType, $this->allowedTypes)) {
throw new Exception('Invalid file type');
}
// 生成随机文件名
$extension = pathinfo($file['name'], PATHINFO_EXTENSION);
$newName = bin2hex(random_bytes(16)) . '.' . $extension;
// 存储到安全目录(非web根目录)
$uploadDir = '/var/www/uploads/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0750, true);
}
// 移动文件
if (!move_uploaded_file($file['tmp_name'], $uploadDir . $newName)) {
throw new Exception('File upload failed');
}
return $newName;
}
}
会话安全
1 安全会话配置
// 会话安全配置
ini_set('session.use_only_cookies', 1);
ini_set('session.cookie_httponly', 1);
ini_set('session.cookie_secure', 1); // HTTPS环境下才启用
ini_set('session.use_strict_mode', 1);
session_start();
// 会话固定防护
if (!isset($_SESSION['initiated'])) {
session_regenerate_id(true);
$_SESSION['initiated'] = true;
}
// 设置会话超时
$_SESSION['last_activity'] = time();
if (isset($_SESSION['last_activity']) && (time() - $_SESSION['last_activity'] > 1800)) {
session_unset();
session_destroy();
header('Location: login.php');
exit;
}
密码安全存储
1 密码哈希
// ✅ 使用password_hash(推荐)
$hashedPassword = password_hash($_POST['password'], PASSWORD_DEFAULT);
// 验证密码
if (password_verify($_POST['password'], $user['password'])) {
// 密码正确
}
// 检查是否需要重新哈希
if (password_needs_rehash($user['password'], PASSWORD_DEFAULT)) {
// 更新密码哈希
}
安全响应头
1 设置安全HTTP头
// 统一设置安全响应头
header('X-Frame-Options: SAMEORIGIN');
header('X-Content-Type-Options: nosniff');
header('X-XSS-Protection: 1; mode=block');
header('Referrer-Policy: strict-origin-when-cross-origin');
header('Content-Security-Policy: default-src \'self\'; script-src \'self\'');
header('Strict-Transport-Security: max-age=31536000; includeSubDomains'); // HTTPS
// 或使用框架中间件
$app->add(new \Middlewares\SecureHeaders());
错误处理与日志
1 安全的错误处理
// 生产环境隐藏错误详情
if (ENVIRONMENT === 'production') {
ini_set('display_errors', 0);
ini_set('log_errors', 1);
ini_set('error_log', '/var/log/php_errors.log');
} else {
ini_set('display_errors', 1);
}
// 自定义错误处理函数
set_exception_handler(function ($exception) {
log_error("Exception: " . $exception->getMessage());
// 生产环境返回通用错误信息
echo '发生错误,请稍后重试';
});
依赖安全
1 使用Composer管理依赖
{
"require": {
"php": ">=7.4",
"ext-pdo": "*",
"ext-filter": "*"
}
}
# 定期更新依赖 composer update --no-dev # 检查已知漏洞 composer audit
十一、安全配置清单
1 PHP配置建议
; php.ini 安全配置 display_errors = Off log_errors = On error_reporting = E_ALL ; 限制上传 upload_max_filesize = 2M max_file_uploads = 20 ; 禁用危险函数 disable_functions = exec,shell_exec,system,passthru,proc_open,show_source ; 会话安全 session.cookie_httponly = 1 session.use_strict_mode = 1
十二、框架安全最佳实践
1 Laravel安全实践
// 使用Eloquent ORM(自动防SQL注入)
$user = User::where('email', $request->email)->first();
// 使用环境变量
$apiKey = env('API_KEY');
// 使用artisan进行安全检查
php artisan security:check
十三、输入数据流程图
用户输入
↓
1. 数据验证(类型、格式、长度)
↓
2. 数据过滤/净化
↓
3. 数据库操作(预处理语句)
↓
4. 输出编码(HTML转义)
↓
5. 安全存储/显示
十四、安全编码检查清单
- [ ] 所有数据库操作使用预处理语句
- [ ] 所有输出进行HTML实体编码
- [ ] 所有表单包含CSRF token
- [ ] 密码使用password_hash()存储
- [ ] 文件上传验证MIME类型和大小
- [ ] 错误信息不泄露敏感信息
- [ ] 会话使用安全配置
- [ ] 输入数据验证和过滤
- [ ] 使用HTTPS加密传输
- [ ] 设置安全响应头
- [ ] 依赖包定期更新和审计
安全编码是一个持续的过程,需要遵循以下核心原则:

- 永远不要信任用户输入
- 使用参数化查询防SQL注入
- 对输出进行编码防XSS
- 使用CSRF token防护
- 最小权限原则
- 安全配置和更新是最重要的防线
安全不是一次性的工作,而是需要在开发的全过程中持续关注和维护。