PHP项目安全基础(一)
安全开发,从每一行代码开始。
一. 屏蔽PHP错误信息
屏蔽错误信息是生产环境的安全基本要求,防止敏感信息(如文件路径、数据库结构、密钥等)泄露给攻击者。
正确做法:通过配置关闭错误显示 + 记录日志
-
方法一:修改 php.ini(推荐,全局生效)
; 关闭错误显示(用户看不到任何错误) display_errors = Off ; 但仍然记录错误到日志(便于排查) log_errors = On error_log = /var/log/php_errors.log ; Linux 路径示例 ; error_log = "C:logsphp_errors.log" ; Windows 路径示例 ; 可选:设置错误报告级别(生产环境建议 E_ALL & ~E_DEPRECATED & ~E_NOTICE) error_reporting = E_ALL & ~E_DEPRECATED & ~E_NOTICE -
方法二:在 PHP 脚本中动态设置(适用于无法改 php.ini 的情况) 在入口文件(如 public/index.php)最顶部添加:
<?php // 关闭错误显示 ini_set('display_errors', '0'); // 开启错误日志 ini_set('log_errors', '1'); // 指定日志文件(可选) ini_set('error_log', __DIR__ . '/../storage/logs/php_error.log'); // 设置错误报告级别(可选) error_reporting(E_ALL & ~E_DEPRECATED & ~E_NOTICE); - 方法三:使用 .htaccess
# 屏蔽 PHP 错误显示 php_flag display_errors off php_flag log_errors on php_value error_log /home/your_user/logs/php_errors.log
二. 防止版本号暴露
防止版本号暴露是安全加固的重要一环。攻击者常通过版本信息判断系统是否存在已知漏洞(如“Apache 2.4.29”、“PHP 7.2.10”、“Laravel v8.12.0”),从而发起针对性攻击。
常见暴露点及防护方法
-
PHP 版本泄露(X-Powered-By 头) PHP 默认会在 HTTP 响应头中添加:
X-Powered-By: PHP/8.1.10解决方案:在 php.ini 中关闭: expose_php = Off -
Web 服务器版本泄露(Apache / Nginx) 问题: Apache 默认返回:Server: Apache/2.4.41 (Ubuntu) Nginx 默认返回:Server: nginx/1.18.0 解决方案:
Apache(在 httpd.conf 或 .htaccess 中): # 隐藏版本号和 OS 信息 ServerTokens Prod ServerSignature Off Nginx(在 nginx.conf 的 http 块中): server_tokens off; -
框架/应用版本泄露 Laravel:调试模式下页面底部显示版本 WordPress:/readme.html、HTML 注释、generator meta 标签 自定义系统:在 JS/CSS 文件名或注释中写死版本(如 app-v2.1.0.js) 防护措施:
**Laravel** APP_DEBUG=false 删除 public/robots.txt 中可能暴露路径的注释 移除或重命名 composer.json(避免被直接访问) **WordPress** 删除 readme.html 在主题 functions.php 中移除 generator: remove_action('wp_head', 'wp_generator'); 使用安全插件(如 Wordfence)隐藏版本 不要在前端资源(JS/CSS/HTML)中硬编码版本号 敏感文件(如 CHANGELOG.md, VERSION, package.json)禁止 Web 访问: # .htaccess 示例 <FilesMatch ".(md|json|yml|yaml|txt)$"> Require all denied </FilesMatch> -
HTTP 响应头中的其他标识
检查是否无意中暴露了技术栈: X-Runtime: 0.023 X-Laravel-Version: 10.12.0 ← 危险! X-Generator: ThinkPHP 6.0清理方法:
// Laravel 中间件示例 public function handle($request, Closure $next) { $response = $next($request); $response->headers->remove('X-Powered-By'); $response->headers->remove('X-Laravel-Version'); // 如果有 return $response; } -
目录列表与默认文件 禁止目录浏览
Options -Indexes删除安装文件(如 install.php, setup/ 目录) 删除示例文件(example/, demo/) 如何检测是否暴露?使用以下命令快速扫描: curl -I https://yourdomain.com查看响应头是否有:
X-Powered-ByServer: Apache/2.x.xX-Generator
三. 防止全局变量覆盖
“防止全局变量覆盖” 主要是指防范 register_globals 机制带来的安全风险(虽然该功能已在 PHP 5.4+ 彻底移除),以及避免因不当使用超全局变量(如 $_GET、$_POST)导致的变量污染问题。 即使现代 PHP 已无 register_globals,但开发者仍可能因 变量命名冲突 或 错误地信任外部输入 而引入类似风险。以下是全面防护指南:
-
确保 register_globals = Off(历史兼容) PHP 5.4.0 起已完全移除 register_globals,无需担心。
但如果你维护的是老旧系统(PHP < 5.4),必须在 php.ini 中设置:
register_globals = Off否则,用户可通过 URL 直接覆盖任意变量: example.com/page.php?isAdmin=1 → 自动创建 $isAdmin = '1' -
永远不要直接使用未声明的变量 危险写法(看似“方便”,实则高危):
// 假设来自 $_GET['action'] if ($action == 'delete') { // $action 未定义!依赖 register_globals 或 extract() delete_user(); }安全写法:显式获取并验证输入
$action = $_GET['action'] ?? null; // 白名单校验 if (in_array($action, ['view', 'edit', 'delete'], true)) { if ($action === 'delete') { delete_user(); } } else { die('非法操作'); } -
避免使用 extract() 函数 extract() 会将数组(如 $_POST)的键作为变量名导入当前作用域,极易导致变量覆盖。 危险示例:
extract($_POST); // 用户提交 password=123 → 自动创建 $password = '123' if ($isAdmin) { // 若用户提交 isAdmin=1,则绕过权限 grant_admin(); }替代方案:明确访问数组元素
$email = $_POST['email'] ?? ''; $password = $_POST['password'] ?? ''; // 不要 extract!如果必须用 extract()(不推荐),至少限制前缀和过滤:
extract($_POST, EXTR_PREFIX_SAME, "user"); // 变量名变为 $user_email, $user_password,降低冲突风险 -
使用严格模式 & 初始化变量 在函数/脚本开头 显式初始化关键变量:
$isAdmin = false; // 先设默认值 if (check_login()) { $isAdmin = current_user_is_admin(); } // 即使 $_GET['isAdmin']=1,也无法覆盖启用 strict_types(PHP 7+)提升类型安全:
declare(strict_types=1); -
框架中的最佳实践 Laravel:通过 Request 对象获取输入
$action = $request->input('action'); // 或验证器 $validated = $request->validate(['action' => 'in:view,edit,delete']);ThinkPHP:使用
$this->request->param() -
静态分析工具检测 使用工具提前发现风险代码:
# 安装 PHPStan composer require --dev phpstan/phpstan # 检查未定义变量使用 vendor/bin/phpstan analyse app/四. 文件系统限制
文件系统限制(Filesystem Restrictions)是防止攻击者通过 Web 应用读取、写入或执行任意服务器文件的关键措施。
-
禁止 Web 进程访问非必要目录
-
限制文件操作范围(沙箱化)
-
防止路径遍历(Path Traversal)攻击
open_basedir 指定 PHP 脚本 只能访问的目录列表,超出范围的操作将被拒绝。 禁用危险函数(Defense in Depth)
在 php.ini 中禁用可能用于文件操作的高危函数: disable_functions = exec,passthru,shell_exec,system,proc_open,popen, curl_exec,curl_multi_exec,parse_ini_file,show_source, symlink,link,pcntl_exec, file_get_contents,file_put_contents,fopen,fread,fwrite安全的文件上传处理 防止路径遍历(Path Traversal) 用户输入中常含 ../ 尝试跳出目录:
$file = $_GET['file']; include("/var/www/templates/$file"); // 攻击:?file=../../../../etc/passwd安全方案 白名单校验:
$allowed = ['header.php', 'footer.php']; if (!in_array($_GET['file'], $allowed, true)) { die('非法文件'); }规范化路径并检查前缀:
$base = '/var/www/templates/'; $path = realpath($base . $_GET['file']); if ($path === false || strpos($path, $base) !== 0) { die('路径非法'); }文件权限最小化原则 使用独立用户运行 Web 服务 不要用 root 运行 PHP-FPM / Apache 创建专用用户(如 www-data),并限制其 Shell 登录:
usermod -s /usr/sbin/nologin www-data
开启PHP 安全模式(Safe Mode)
在共享主机环境中,防止一个用户的 PHP 脚本访问/修改其他用户的文件。 实现方式:通过限制文件操作、执行命令、环境变量等来“沙箱化”脚本。
示例(PHP < 5.4):
```
safe_mode = On
safe_mode_gid = On
```
为什么被废弃?
- 安全效果有限 Safe Mode 无法防御大多数真实攻击(如 SQL 注入、XSS、文件包含漏洞)。
- 绕过方法众多 攻击者可通过 .htaccess、第三方扩展、PHP 配置覆盖等方式绕过限制。
- 维护复杂,误报率高 正常应用(如 WordPress 插件)常因 Safe Mode 无法写入缓存而崩溃。
- 真正的解决方案是系统级隔离 如:使用独立虚拟机、容器(Docker)、chroot、Linux 用户权限控制。