Skip to content

安全总则

PHP Web 应用的安全性是一个系统性的工程,涵盖输入验证、输出编码、认证授权、数据保护、服务器配置等多个方面。本节从全局视角介绍 PHP 安全编程的核心原则,为后续各专题章节提供整体框架和指导方针。

前置知识

阅读本节前,建议先了解:HTTP 协议概览表单提交与验证

基础概念

安全威胁分类

威胁类型攻击目标防御方式
XSS(跨站脚本)用户浏览器输出编码、CSP
SQL 注入数据库预处理语句、参数化查询
CSRF(跨站请求伪造)用户会话Token、SameSite
文件上传漏洞服务器文件系统类型检测、重命名
Session 劫持用户身份HTTPS、HttpOnly、指纹
目录遍历服务器文件系统路径验证、白名单
命令注入服务器操作系统避免使用、参数化
反序列化漏洞应用逻辑避免反序列化不可信数据

OWASP Top 10 (2021)

排名风险PHP 防御
A01Broken Access Control权限检查、RBAC
A02Cryptographic FailuresHTTPS、强加密
A03Injection预处理语句、filter_input
A04Insecure Design安全设计原则
A05Security Misconfiguration安全 php.ini 配置
A06Vulnerable ComponentsComposer audit、更新依赖
A07Auth Failures强认证、限速
A08Software & Data Integrity签名验证
A09Logging & Monitoring审计日志
A10SSRFURL 白名单、禁用内部请求

核心安全原则

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/exec

2. 最小权限原则

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=6

2. 安全检查清单

下一节

继续学习:输入过滤与验证

参考链接