安全总则
PHP Web 应用的安全性是一个系统性的工程,涵盖输入验证、输出编码、认证授权、数据保护、服务器配置等多个方面。本节从全局视角介绍 PHP 安全编程的核心原则,为后续各专题章节提供整体框架和指导方针。
基础概念
安全威胁分类
| 威胁类型 | 攻击目标 | 防御方式 |
|---|---|---|
| XSS(跨站脚本) | 用户浏览器 | 输出编码、CSP |
| SQL 注入 | 数据库 | 预处理语句、参数化查询 |
| CSRF(跨站请求伪造) | 用户会话 | Token、SameSite |
| 文件上传漏洞 | 服务器文件系统 | 类型检测、重命名 |
| Session 劫持 | 用户身份 | HTTPS、HttpOnly、指纹 |
| 目录遍历 | 服务器文件系统 | 路径验证、白名单 |
| 命令注入 | 服务器操作系统 | 避免使用、参数化 |
| 反序列化漏洞 | 应用逻辑 | 避免反序列化不可信数据 |
OWASP Top 10 (2021)
| 排名 | 风险 | PHP 防御 |
|---|---|---|
| A01 | Broken Access Control | 权限检查、RBAC |
| A02 | Cryptographic Failures | HTTPS、强加密 |
| A03 | Injection | 预处理语句、filter_input |
| A04 | Insecure Design | 安全设计原则 |
| A05 | Security Misconfiguration | 安全 php.ini 配置 |
| A06 | Vulnerable Components | Composer audit、更新依赖 |
| A07 | Auth Failures | 强认证、限速 |
| A08 | Software & Data Integrity | 签名验证 |
| A09 | Logging & Monitoring | 审计日志 |
| A10 | SSRF | URL 白名单、禁用内部请求 |
核心安全原则
1. 永远不信任用户输入
php
<?php
declare(strict_types=1);
// 错误:直接使用用户输入
$id = $_GET['id'];
$sql = "SELECT * FROM users WHERE id = $id"; // SQL 注入!
$name = $_GET['name'];
echo "<p>Hello, $name</p>"; // XSS!
$file = $_GET['file'];
include("/var/www/$file"); // 文件包含漏洞!
$cmd = $_GET['cmd'];
system("ping $cmd"); // 命令注入!
// 正确:验证、过滤、编码
$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
$name = htmlspecialchars($_GET['name'] ?? '', ENT_QUOTES, 'UTF-8');
$file = basename(filter_var($_GET['file'] ?? '', FILTER_SANITIZE_STRING));
// 不要使用 system/exec2. 最小权限原则
php
<?php
// 错误:数据库使用 root 用户连接
$pdo = new PDO('mysql:host=localhost;dbname=app', 'root', 'password');
// 正确:使用专用低权限用户
$pdo = new PDO('mysql:host=localhost;dbname=app', 'app_user', 'strong_password');
// app_user 只有 SELECT, INSERT, UPDATE, DELETE 权限
// 没有 DROP, ALTER, CREATE, GRANT 等权限3. 深度防御(多层防护)
php
<?php
// 一层防护可能失效,多层防护提高安全性
// 例如:防止 SQL 注入
// 第一层:输入验证(确认是整数)
$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
// 第二层:预处理语句(参数化查询)
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = ?');
$stmt->execute([$id]);
// 第三层:最小权限数据库用户(无 DROP/ALTER 权限)PHP 安全配置
php.ini 安全设置
ini
; === 基本安全设置 ===
expose_php = Off ; 隐藏 PHP 版本
display_errors = Off ; 关闭错误显示(生产环境)
log_errors = On ; 开启错误日志
error_log = /var/log/php_errors.log
display_startup_errors = Off
; === 资源限制 ===
memory_limit = 128M ; 内存限制
max_execution_time = 30 ; 最大执行时间
max_input_time = 60 ; 最大输入时间
post_max_size = 8M ; POST 最大大小
upload_max_filesize = 2M ; 上传文件大小
max_file_uploads = 20 ; 最大上传文件数
; === Session 安全 ===
session.use_strict_mode = 1 ; 严格模式(PHP 7.1+)
session.use_only_cookies = 1 ; 仅使用 Cookie 传输 SID
session.cookie_httponly = 1 ; HttpOnly Cookie
session.cookie_secure = 1 ; Secure Cookie(HTTPS)
session.cookie_samesite = Lax ; SameSite 属性
session.gc_maxlifetime = 7200 ; Session 过期时间
session.sid_length = 48 ; Session ID 长度(PHP 7.1+)
session.sid_bits_per_character = 6
; === 禁用危险函数(按需)===
disable_functions = exec,passthru,shell_exec,system,proc_open,popen,
curl_multi_exec,parse_ini_file,show_source
; === 开启 OPCache(性能+安全)===
opcache.enable = 1
opcache.validate_timestamps = 0 ; 生产环境关闭(防止篡改)Web 服务器安全配置
apache
# Apache .htaccess 安全配置
# 禁止目录列表
Options -Indexes
# 禁止访问敏感文件
<FilesMatch "\.(env|log|ini|sh|sql|bak|swp)$">
Order allow,deny
Deny from all
</FilesMatch>
# 禁止访问 .git 目录
RedirectMatch 404 /\.git
# 隐藏服务器信息
ServerTokens Prod
ServerSignature Off
# 安全头部
Header set X-Content-Type-Options "nosniff"
Header set X-Frame-Options "SAMEORIGIN"
Header set X-XSS-Protection "1; mode=block"
Header set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"nginx
# Nginx 安全配置
# 隐藏版本号
server_tokens off;
# 禁止访问敏感文件
location ~ /\.(env|log|ini|sh|sql|bak|swp|git) {
deny all;
return 404;
}
# 安全头部
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# 限制请求体大小
client_max_body_size 8m;
# 禁止执行上传目录中的 PHP
location ~* /uploads/.*\.php$ {
deny all;
}错误处理与安全
生产环境错误处理
php
<?php
declare(strict_types=1);
/**
* 生产环境安全错误处理
*/
// 关闭错误显示
ini_set('display_errors', '0');
ini_set('display_startup_errors', '0');
// 开启错误日志
ini_set('log_errors', '1');
ini_set('error_log', '/var/log/php_errors.log');
// 自定义错误处理
set_error_handler(function (int $severity, string $message, string $file, int $line): bool {
// 不向用户显示详细错误
error_log("[{$severity}] {$message} in {$file}:{$line}");
// 安全敏感信息不出现在日志中
$safeMessage = str_replace(
[DB_PASSWORD, API_KEY, SECRET],
['*****', '*****', '*****'],
$message
);
error_log($safeMessage);
return true; // 阻止默认错误处理器
});
// 自定义异常处理
set_exception_handler(function (Throwable $e): void {
error_log("Uncaught exception: {$e->getMessage()} in {$e->getFile()}:{$e->getLine()}");
http_response_code(500);
echo '服务器内部错误,请稍后重试';
exit;
});
// 注册关闭函数(捕获致命错误)
register_shutdown_function(function (): void {
$error = error_get_last();
if ($error !== null && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR], true)) {
error_log("Fatal error: {$error['message']} in {$error['file']}:{$error['line']}");
http_response_code(500);
}
});开发环境 vs 生产环境
php
<?php
declare(strict_types=1);
/**
* 根据环境切换错误配置
*/
function setupErrorHandling(string $env): void
{
if ($env === 'development') {
error_reporting(E_ALL);
ini_set('display_errors', '1');
ini_set('display_startup_errors', '1');
} else {
error_reporting(E_ALL & ~E_DEPRECATED & ~E_STRICT);
ini_set('display_errors', '0');
ini_set('display_startup_errors', '0');
ini_set('log_errors', '1');
}
}
setupErrorHandling(getenv('APP_ENV') ?: 'production');安全响应头
php
<?php
declare(strict_types=1);
/**
* 设置安全响应头
*/
function setSecurityHeaders(bool $isHttps): void
{
// 防止 MIME 类型嗅探
header('X-Content-Type-Options: nosniff');
// 防止点击劫持
header('X-Frame-Options: SAMEORIGIN');
// XSS 防护(旧浏览器)
header('X-XSS-Protection: 1; mode=block');
// Referrer 策略
header('Referrer-Policy: strict-origin-when-cross-origin');
// 隐藏 PHP 信息
header_remove('X-Powered-By');
if ($isHttps) {
// 强制 HTTPS
header('Strict-Transport-Security: max-age=31536000; includeSubDomains');
}
}
$isHttps = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
|| ($_SERVER['SERVER_PORT'] ?? 80) == 443;
setSecurityHeaders($isHttps);注意事项
1. 不要在代码中硬编码敏感信息
php
<?php
// 错误:硬编码密码
$pdo = new PDO('mysql:host=localhost;dbname=app', 'root', 'myPassword123');
// 正确:使用环境变量
$host = getenv('DB_HOST');
$user = getenv('DB_USER');
$pass = getenv('DB_PASSWORD');
$pdo = new PDO("mysql:host={$host};dbname=app", $user, $pass);
// 更好:使用 .env 文件(不提交到版本控制)
// Composer: vlucas/phpdotenv
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->load();2. 不要在版本控制中存储敏感文件
# .gitignore
.env
.env.local
config/production.php
*.key
*.pem
database/*.sql最佳实践
1. 定期安全审计
bash
# Composer 依赖安全检查
composer audit
# 已知漏洞检查
composer require --dev roave/security-advisories:dev-latest
# PHP 代码静态分析
composer require --dev phpstan/phpstan
vendor/bin/phpstan analyse src --level=62. 安全检查清单
下一节
继续学习:输入过滤与验证