PHP项目安全基础(一) - Zanealancy博客

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”),从而发起针对性攻击。

常见暴露点及防护方法

  1. PHP 版本泄露(X-Powered-By 头) PHP 默认会在 HTTP 响应头中添加:X-Powered-By: PHP/8.1.10 解决方案:

    在 php.ini 中关闭:
    expose_php = Off
  2. 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;
  3. 框架/应用版本泄露 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>
  4. 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;
    }
  5. 目录列表与默认文件 禁止目录浏览 Options -Indexes 删除安装文件(如 install.php, setup/ 目录) 删除示例文件(example/, demo/) 如何检测是否暴露?

    使用以下命令快速扫描: 
    curl -I https://yourdomain.com

    查看响应头是否有: X-Powered-By Server: Apache/2.x.x X-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 用户权限控制。