七、PHP配置(php.ini)安全最佳实践

1. PHP配置体系结构

1.1 配置文件加载顺序

PHP配置可以来自多个位置,理解加载顺序对安全至关重要。

复制代码
加载顺序(后面的覆盖前面的):
1. 编译时默认值
2. php.ini (主配置文件)
3. php.ini 中的 [HOST=section] 和 [PATH=section]
4. php-fpm.conf 中的 php_admin_value/php_value
5. httpd.conf / .htaccess (Apache)
6. 运行时 ini_set()

PHP_INI 模式说明:

模式 常量 含义 典型配置项
USER PHP_INI_USER 脚本中可用 ini_set() 修改 error_reporting, display_errors
PERDIR PHP_INI_PERDIR php.ini, .htaccess, httpd.conf 可修改 include_path, auto_prepend_file
SYSTEM PHP_INI_SYSTEM php.ini 和系统级配置可修改 disable_functions, extension_dir
ALL PHP_INI_ALL 任何地方都可修改 大多数运行时配置

安全检查要点:

  • 🔴 检查 .htaccess 是否允许覆盖关键配置
  • 🔴 检查用户是否能在脚本中修改安全相关配置
  • 🟢 使用 php_admin_value (PHP-FPM) 强制配置,禁止覆盖

1.2 配置查看与诊断

php 复制代码
<?php
/**
 * 安全配置审计脚本
 */

class SecurityConfigAuditor {
    private $criticalSettings = [
        'disable_functions' => ['expected' => 'not_empty', 'risk' => 'high'],
        'open_basedir' => ['expected' => 'not_empty', 'risk' => 'high'],
        'expose_php' => ['expected' => 'Off', 'risk' => 'medium'],
        'display_errors' => ['expected' => 'Off', 'risk' => 'high'],
        'allow_url_fopen' => ['expected' => 'Off', 'risk' => 'high'],
        'allow_url_include' => ['expected' => 'Off', 'risk' => 'critical'],
        'session.cookie_httponly' => ['expected' => '1', 'risk' => 'high'],
        'session.cookie_secure' => ['expected' => '1', 'risk' => 'high'],
        'session.use_strict_mode' => ['expected' => '1', 'risk' => 'medium'],
    ];

    public function audit(): array {
        $results = [];

        foreach ($this->criticalSettings as $setting => $check) {
            $current = ini_get($setting);
            $expected = $check['expected'];

            $status = match($expected) {
                'not_empty' => !empty($current) && $current !== '' && $current !== false,
                'On', '1' => $current === '1' || strtolower($current) === 'on',
                'Off', '0' => $current === '' || $current === '0' || strtolower($current) === 'off',
                default => $current === $expected
            };

            $results[$setting] = [
                'current' => var_export($current, true),
                'expected' => $expected,
                'status' => $status ? 'PASS' : 'FAIL',
                'risk' => $check['risk']
            ];
        }

        return $results;
    }

    public function generateReport(): string {
        $results = $this->audit();
        $report = "PHP Security Configuration Audit\n";
        $report .= str_repeat('=', 50) . "\n\n";

        foreach ($results as $setting => $result) {
            $icon = $result['status'] === 'PASS' ? '✓' : '✗';
            $report .= "$icon [$result[risk]] $setting\n";
            $report .= "   Current:  {$result['current']}\n";
            $report .= "   Expected: {$result['expected']}\n\n";
        }

        return $report;
    }
}

// 使用
$auditor = new SecurityConfigAuditor();
echo $auditor->generateReport();
?>

2. 核心安全配置详解

2.1 信息披露控制

ini 复制代码
; =====================================================
; 信息披露控制 - 减少攻击面信息
; =====================================================

; 不暴露PHP版本信息到HTTP头(X-Powered-By)
expose_php = Off

; 不在错误信息中显示PHP版本
; 注意:这也会移除phpinfo()中的logo

; 关闭详细错误显示(生产环境必须)
display_errors = Off
display_startup_errors = Off

; 开启错误日志记录
log_errors = On

; 设置日志路径(确保路径安全)
error_log = /var/log/php/error.log

; 限制错误日志长度,防止日志膨胀
log_errors_max_len = 1024

; 不记录重复错误
ignore_repeated_errors = On
ignore_repeated_source = On

; 错误报告级别(生产环境)
error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT

; 禁用HTML格式的错误(防止XSS)
html_errors = Off

; 不显示传递给错误处理器的参数
; 防止敏感信息泄露
docref_root = ""
docref_ext = ""

2.2 文件系统隔离

ini 复制代码
; =====================================================
; 文件系统隔离 - 限制PHP的文件访问范围
; =====================================================

; 限制PHP只能访问指定目录下的文件
; 多个目录用冒号(Linux)或分号(Windows)分隔
open_basedir = /var/www/html:/tmp:/var/log/php

; 禁止访问时记录警告
; 这有助于检测攻击尝试

; 上传文件临时目录
upload_tmp_dir = /var/www/tmp

; 会话文件存储目录
session.save_path = "/var/www/sessions"

; 确保这些目录的权限正确
; 750 (drwxr-x---) 推荐

open_basedir 注意事项:

  • ✅ 必须包含上传目录、会话目录、临时目录
  • ✅ 不应包含系统目录(/etc, /usr等)
  • ⚠️ 可以被某些方式绕过(如chdir())
  • ⚠️ 不影响系统命令执行函数

2.3 远程访问控制

ini 复制代码
; =====================================================
; 远程访问控制 - 防止远程文件包含和代码注入
; =====================================================

; 禁用通过URL打开文件(强烈推荐)
; 这能阻止RFI(远程文件包含)攻击
allow_url_fopen = Off

; 绝对禁止通过URL包含文件
disable_url_include = Off
allow_url_include = Off

; 注意:即使开启,也应该配合白名单使用
; allow_url_fopen = On
; 代码中验证URL: if (strpos($url, 'https://trusted-domain.com') !== 0) { reject }

; 限制用户代理(可选,可阻止某些扫描器)
; user_agent = "MyApp/1.0"

; 设置默认超时
default_socket_timeout = 30

2.4 动态代码执行控制

ini 复制代码
; =====================================================
; 动态代码执行控制 - 限制代码执行能力
; =====================================================

; 禁用危险函数(关键安全措施)
disable_functions =
    exec, passthru, shell_exec, system,
    proc_open, proc_close, proc_get_status, proc_nice,
    proc_terminate, popen, pclose,
    pcntl_exec, pcntl_fork, pcntl_signal, pcntl_waitpid,
    dl, posix_kill, posix_mkfifo, posix_setpgid,
    posix_setsid, posix_setuid, posix_seteuid,
    escapeshellcmd, escapeshellarg,
    chown, chgrp, chmod,
    symlink, link,
    apache_child_terminate, apache_get_modules,
    apache_get_version, apache_getenv, apache_note,
    define_syslog_variables, eval, assert

; 注意:eval和assert无法真正禁用,但加入此列表会在调用时产生警告
; 真正的防御需要代码层面禁用

; 禁用类(PHP 7.4+)
disable_classes =

; 禁用危险魔术方法(通过Suhosin扩展)
; suhosin.executor.disable_emodifier = On

3. 函数禁用策略

3.1 危险函数分类

php 复制代码
<?php
/**
 * 危险函数分类清单
 */

class DangerousFunctions {
    // 命令执行类(最高危险)
    public static $commandExecution = [
        'exec',        // 执行系统命令
        'passthru',    // 执行并输出结果
        'shell_exec',  // 通过shell执行
        'system',      // 执行并输出
        'proc_open',   // 打开进程
        'popen',       // 打开管道
        'pcntl_exec',  // 替换进程(CLI)
    ];

    // 代码执行类(最高危险)
    public static $codeExecution = [
        'eval',        // 执行PHP代码
        'assert',      // PHP < 7.2 可执行代码
        'create_function', // 创建匿名函数(已弃用)
        'preg_replace' => '/e', // /e修饰符(PHP 5.x)
    ];

    // 文件系统操作类(中高危险)
    public static $filesystem = [
        'chown',       // 改变文件所有者
        'chgrp',       // 改变文件组
        'chmod',       // 改变文件权限
        'symlink',     // 创建符号链接
        'link',        // 创建硬链接
        'fileperms',   // 获取文件权限(信息泄露)
    ];

    // 进程控制类(高危险)
    public static $processControl = [
        'pcntl_fork',
        'pcntl_signal',
        'pcntl_alarm',
        'pcntl_waitpid',
        'posix_kill',
        'posix_mkfifo',
        'posix_setuid',
        'posix_seteuid',
        'posix_setgid',
        'posix_setegid',
        'posix_setsid',
    ];

    // 信息收集类(中危险)
    public static $information = [
        'phpinfo',     // PHP配置信息
        'get_cfg_var', // 获取配置值
        'get_current_user',
        'getmyuid',
        'getmygid',
        'getmyinode',
        'getmypid',
        'getrusage',
        'php_uname',
        'posix_getpwuid',
        'posix_getgrgid',
    ];

    // 扩展加载类(中危险)
    public static $extension = [
        'dl',          // 动态加载扩展
    ];

    /**
     * 生成disable_functions字符串
     */
    public static function generateDisableList(array $categories): string {
        $functions = [];

        foreach ($categories as $category) {
            if (isset(self::$$category)) {
                $functions = array_merge($functions, self::$$category);
            }
        }

        return implode(',', array_unique($functions));
    }
}

// 生成配置
echo DangerousFunctions::generateDisableList([
    'commandExecution',
    'processControl',
    'filesystem'
]);
?>

3.2 场景化函数禁用

ini 复制代码
; =====================================================
; 场景化函数禁用配置
; =====================================================

; 场景1: 共享主机(最严格)
disable_functions =
    exec, passthru, shell_exec, system, proc_open, proc_close,
    proc_get_status, proc_nice, proc_terminate, popen, pclose,
    pcntl_exec, pcntl_fork, pcntl_signal, pcntl_alarm,
    pcntl_waitpid, posix_kill, posix_mkfifo, posix_setpgid,
    posix_setsid, posix_setuid, posix_seteuid, posix_setgid,
    posix_setegid, dl, chown, chgrp, chmod, symlink, link,
    fileperms, phpinfo, get_current_user, getmyuid, getmygid,
    getmyinode, getmypid, getrusage, php_uname,
    apache_child_terminate, apache_get_modules, apache_get_version,
    apache_getenv, apache_note, define_syslog_variables,
    eval, assert, create_function, ini_restore

; 场景2: 标准Web应用(推荐)
disable_functions =
    exec, passthru, shell_exec, system, proc_open, proc_nice,
    proc_terminate, popen, pcntl_exec, pcntl_fork, pcntl_signal,
    pcntl_waitpid, posix_kill, posix_mkfifo, posix_setuid,
    posix_seteuid, dl, chown, symlink, link

; 场景3: 企业应用(较宽松,需配合监控)
disable_functions =
    exec, passthru, shell_exec, system, pcntl_exec, pcntl_fork,
    posix_setuid, posix_seteuid, dl

3.3 函数禁用的替代方案

php 复制代码
<?php
/**
 * 禁用函数后的替代实现
 */

// 原:exec('ls -la', $output);
// 替代:使用PHP原生函数
function safeListDirectory(string $path): array {
    if (!is_dir($path)) {
        throw new InvalidArgumentException("Not a directory");
    }

    $files = [];
    $iterator = new DirectoryIterator($path);
    foreach ($iterator as $fileinfo) {
        $files[] = [
            'name' => $fileinfo->getFilename(),
            'size' => $fileinfo->getSize(),
            'modified' => $fileinfo->getMTime(),
            'type' => $fileinfo->getType(),
            'permissions' => substr(sprintf('%o', $fileinfo->getPerms()), -4),
        ];
    }
    return $files;
}

// 原:fileperms('/path/to/file')
// 替代:使用stat()
function safeGetPermissions(string $path): ?string {
    if (!file_exists($path)) {
        return null;
    }
    $stat = stat($path);
    return ($stat === false) ? null : substr(sprintf('%o', $stat['mode']), -4);
}

// 原:symlink('/target', '/link')
// 替代:应用程序逻辑实现软链接功能
class VirtualSymlink {
    private $mappings = [];

    public function register(string $link, string $target): void {
        $realTarget = realpath($target);
        if ($realTarget === false) {
            throw new InvalidArgumentException("Target does not exist");
        }
        $this->mappings[realpath($link)] = $realTarget;
    }

    public function resolve(string $path): string {
        $realPath = realpath($path);
        foreach ($this->mappings as $link => $target) {
            if (strpos($realPath, $link) === 0) {
                return $target . substr($realPath, strlen($link));
            }
        }
        return $path;
    }
}
?>

4. 资源限制与防护

4.1 执行时间与内存限制

ini 复制代码
; =====================================================
; 执行时间与内存限制 - 防止拒绝服务攻击
; =====================================================

; 脚本最大执行时间(秒)
; 0表示无限制(危险)
max_execution_time = 30

; 脚本最大CPU时间(秒)
; 防止无限循环消耗CPU
max_input_time = 60

; 脚本可用内存限制
; 防止内存耗尽攻击
memory_limit = 128M

; 最大输入变量数量
; 防止哈希碰撞攻击
max_input_vars = 1000

; 最大输入嵌套层级
; 防止序列化炸弹
max_input_nesting_level = 64

; POST数据最大大小
; 防止大文件上传DOS
post_max_size = 8M

; 文件上传最大大小
upload_max_filesize = 2M

; 最大文件上传数量
max_file_uploads = 20

; 每个请求的最大时间
max_execution_time = 30

; 实时进程优先级(Linux)
; 降低PHP进程的CPU优先级
; nice(19) = 最低优先级

4.2 输入数据处理

ini 复制代码
; =====================================================
; 输入数据处理 - 防止输入相关的攻击
; =====================================================

; 关闭自动全局变量(已废弃,保持Off)
register_globals = Off

; 关闭魔术引号(已废弃,保持Off)
magic_quotes_gpc = Off

; 关闭自动转义(已废弃)
magic_quotes_runtime = Off

; 关闭SVG支持(防止XXE)
; libxml_disable_entity_loader = On(PHP 8.0+已移除)

; 过滤非法字符
; 注意:这可能破坏某些合法输入
; filter.default = "special_chars"

; 最大请求时间
; 防止慢速HTTP攻击
; 需要在Web服务器层配置

4.3 资源限制代码实现

php 复制代码
<?php
/**
 * 运行时资源限制控制
 */

class ResourceLimiter {
    /**
     * 设置安全资源限制
     */
    public static function applySafeLimits(): void {
        // 执行时间限制(可在运行时修改)
        set_time_limit(30);

        // 内存限制(仅可增加)
        // ini_set('memory_limit', '128M');

        // 设置最大嵌套层级
        ini_set('max_input_nesting_level', '64');

        // 设置最大输入变量
        ini_set('max_input_vars', '1000');
    }

    /**
     * 针对特定操作放宽限制
     */
    public static function withExtendedLimit(callable $operation, int $timeLimit = 300, string $memoryLimit = '512M') {
        $originalTime = ini_get('max_execution_time');
        $originalMemory = ini_get('memory_limit');

        try {
            set_time_limit($timeLimit);
            ini_set('memory_limit', $memoryLimit);

            return $operation();
        } finally {
            // 恢复原有限制
            set_time_limit($originalTime);
            ini_set('memory_limit', $originalMemory);
        }
    }

    /**
     * 检测资源使用情况
     */
    public static function checkResourceUsage(): array {
        return [
            'memory_current' => memory_get_usage(true),
            'memory_peak' => memory_get_peak_usage(true),
            'memory_limit' => ini_get('memory_limit'),
            'execution_time' => microtime(true) - $_SERVER['REQUEST_TIME_FLOAT'],
            'time_limit' => ini_get('max_execution_time'),
        ];
    }

    /**
     * 安全地处理大文件
     */
    public static function processLargeFile(string $path, callable $processor, int $chunkSize = 8192): void {
        $handle = fopen($path, 'r');
        if (!$handle) {
            throw new RuntimeException("Cannot open file");
        }

        $bytesProcessed = 0;
        $maxBytes = self::parseSize(ini_get('memory_limit')) * 0.5; // 50% of memory limit

        try {
            while (!feof($handle)) {
                $chunk = fread($handle, $chunkSize);
                $bytesProcessed += strlen($chunk);

                if ($bytesProcessed > $maxBytes) {
                    throw new RuntimeException("File too large to process");
                }

                $processor($chunk);
            }
        } finally {
            fclose($handle);
        }
    }

    private static function parseSize(string $size): int {
        $unit = strtolower(substr($size, -1));
        $value = (int) $size;

        return match($unit) {
            'g' => $value * 1024 * 1024 * 1024,
            'm' => $value * 1024 * 1024,
            'k' => $value * 1024,
            default => $value
        };
    }
}
?>

5. 会话安全配置

5.1 会话基础安全

ini 复制代码
; =====================================================
; 会话安全配置 - 防止会话劫持和固定攻击
; =====================================================

; 会话处理程序
; 推荐:使用数据库或Redis替代文件
session.save_handler = files
session.save_path = "/var/www/sessions"

; 会话名称(避免使用默认PHPSESSID)
session.name = "SESSID"

; 仅通过Cookie传递会话ID
session.use_only_cookies = 1

; 不使用透明会话ID(URL传递)
session.use_trans_sid = 0

; 严格会话模式
; 拒绝未初始化的会话ID
session.use_strict_mode = 1

; Cookie生存时间(秒)
; 0 = 浏览器关闭时过期
session.cookie_lifetime = 0

; Cookie路径
session.cookie_path = /

; Cookie域(根据实际配置)
; session.cookie_domain = ".example.com"

; 仅通过HTTPS发送Cookie
session.cookie_secure = 1

; Cookie HttpOnly标志(防止XSS窃取)
session.cookie_httponly = 1

; Cookie SameSite属性(PHP 7.3+)
; Strict: 完全禁止跨站发送
; Lax: 允许安全HTTP方法跨站
; None: 允许跨站(需配合Secure)
session.cookie_samesite = "Strict"

; 会话垃圾回收概率
; gc_probability / gc_divisor = 清理概率
session.gc_probability = 1
session.gc_divisor = 1000

; 会话最大生存时间(秒)
; 超过此时间的数据将被清理
session.gc_maxlifetime = 1440

; 会话数据压缩
; 对于大数据会话有用
; session.serialize_handler = "php_serialize"

5.2 高级会话安全

php 复制代码
<?php
/**
 * 会话安全增强类
 */

class SecureSession {
    /**
     * 安全初始化会话
     */
    public static function start(): void {
        // 设置安全参数(必须在session_start之前)
        ini_set('session.cookie_httponly', '1');
        ini_set('session.cookie_secure', '1');
        ini_set('session.use_only_cookies', '1');
        ini_set('session.use_strict_mode', '1');

        if (PHP_VERSION_ID >= 70300) {
            ini_set('session.cookie_samesite', 'Strict');
        }

        session_start();

        // 安全检测
        self::securityChecks();
    }

    /**
     * 会话安全检查
     */
    private static function securityChecks(): void {
        // 检查IP绑定(可选)
        if (isset($_SESSION['ip_address'])) {
            if ($_SESSION['ip_address'] !== $_SERVER['REMOTE_ADDR']) {
                // IP变化,可能是会话劫持
                self::destroy();
                throw new SecurityException("Session IP mismatch");
            }
        } else {
            $_SESSION['ip_address'] = $_SERVER['REMOTE_ADDR'];
        }

        // 检查User-Agent绑定(可选,但注意代理可能改变UA)
        if (isset($_SESSION['user_agent'])) {
            if ($_SESSION['user_agent'] !== $_SERVER['HTTP_USER_AGENT']) {
                self::destroy();
                throw new SecurityException("Session UA mismatch");
            }
        } else {
            $_SESSION['user_agent'] = $_SERVER['HTTP_USER_AGENT'] ?? 'unknown';
        }

        // 会话超时检查
        if (isset($_SESSION['last_activity'])) {
            $inactive = time() - $_SESSION['last_activity'];
            if ($inactive > 1800) { // 30分钟
                self::destroy();
                throw new SecurityException("Session timed out");
            }
        }
        $_SESSION['last_activity'] = time();
    }

    /**
     * 重新生成会话ID(登录后调用)
     */
    public static function regenerateId(bool $deleteOld = true): void {
        session_regenerate_id($deleteOld);
    }

    /**
     * 安全销毁会话
     */
    public static function destroy(): void {
        // 清空会话数据
        $_SESSION = [];

        // 删除Cookie
        if (ini_get("session.use_cookies")) {
            $params = session_get_cookie_params();
            setcookie(
                session_name(),
                '',
                time() - 42000,
                $params["path"],
                $params["domain"],
                $params["secure"],
                $params["httponly"]
            );
        }

        // 销毁会话
        session_destroy();
    }

    /**
     * 设置会话值(自动序列化复杂类型)
     */
    public static function set(string $key, $value): void {
        $_SESSION[$key] = $value;
    }

    /**
     * 获取会话值
     */
    public static function get(string $key, $default = null) {
        return $_SESSION[$key] ?? $default;
    }

    /**
     * 删除会话值
     */
    public static function delete(string $key): void {
        unset($_SESSION[$key]);
    }
}
?>

6. 上传与文件安全

6.1 文件上传配置

ini 复制代码
; =====================================================
; 文件上传安全配置
; =====================================================

; 是否允许文件上传
file_uploads = On

; 临时上传目录
; 确保此目录不在web根目录下,且定期清理
upload_tmp_dir = /var/www/tmp

; 最大上传文件大小
upload_max_filesize = 2M

; 最大POST数据大小(必须大于upload_max_filesize)
post_max_size = 8M

; 最大文件上传数量
max_file_uploads = 20

; 是否检查MIME类型
; 注意:MIME类型可以被伪造
; fileinfo extension 提供更可靠的检测

; 上传文件用户组(需要编译时支持)
; upload_tmp_dir 的权限应该只允许PHP用户访问

6.2 文件上传安全代码

php 复制代码
<?php
/**
 * 安全文件上传处理
 */

class SecureFileUpload {
    private $allowedTypes = [
        'image/jpeg' => 'jpg',
        'image/png' => 'png',
        'image/gif' => 'gif',
        'application/pdf' => 'pdf',
    ];

    private $maxSize;
    private $uploadPath;

    public function __construct(string $uploadPath, int $maxSize = 2097152) {
        $this->uploadPath = rtrim($uploadPath, '/');
        $this->maxSize = $maxSize;

        if (!is_dir($this->uploadPath)) {
            throw new RuntimeException("Upload directory does not exist");
        }
    }

    /**
     * 处理上传
     */
    public function handle(array $file): array {
        // 检查上传错误
        if ($file['error'] !== UPLOAD_ERR_OK) {
            throw new UploadException($this->getErrorMessage($file['error']));
        }

        // 验证文件大小
        if ($file['size'] > $this->maxSize) {
            throw new UploadException("File too large");
        }

        // 验证MIME类型
        $mimeType = $this->getMimeType($file['tmp_name']);
        if (!isset($this->allowedTypes[$mimeType])) {
            throw new UploadException("Invalid file type");
        }

        // 验证文件扩展名
        $extension = $this->allowedTypes[$mimeType];

        // 生成安全文件名
        $filename = $this->generateFilename($extension);
        $destination = $this->uploadPath . '/' . $filename;

        // 移动文件
        if (!move_uploaded_file($file['tmp_name'], $destination)) {
            throw new UploadException("Failed to save file");
        }

        // 设置安全权限
        chmod($destination, 0640);

        return [
            'filename' => $filename,
            'original_name' => $file['name'],
            'size' => $file['size'],
            'mime_type' => $mimeType,
            'path' => $destination,
        ];
    }

    /**
     * 获取真实的MIME类型
     */
    private function getMimeType(string $path): string {
        $finfo = finfo_open(FILEINFO_MIME_TYPE);
        $mime = finfo_file($finfo, $path);
        finfo_close($finfo);
        return $mime;
    }

    /**
     * 生成安全文件名
     */
    private function generateFilename(string $extension): string {
        return bin2hex(random_bytes(16)) . '.' . $extension;
    }

    /**
     * 获取错误信息
     */
    private function getErrorMessage(int $code): string {
        return match($code) {
            UPLOAD_ERR_INI_SIZE => "File exceeds upload_max_filesize",
            UPLOAD_ERR_FORM_SIZE => "File exceeds MAX_FILE_SIZE",
            UPLOAD_ERR_PARTIAL => "File partially uploaded",
            UPLOAD_ERR_NO_FILE => "No file uploaded",
            UPLOAD_ERR_NO_TMP_DIR => "Missing temporary folder",
            UPLOAD_ERR_CANT_WRITE => "Failed to write file",
            UPLOAD_ERR_EXTENSION => "Upload stopped by extension",
            default => "Unknown upload error"
        };
    }
}

class UploadException extends Exception {}
?>

7. 场景化配置模板

7.1 生产环境配置模板

ini 复制代码
; =====================================================
; PHP生产环境安全配置模板
; =====================================================

[PHP]
; 信息披露
expose_php = Off
display_errors = Off
display_startup_errors = Off
log_errors = On
html_errors = Off
error_log = /var/log/php/error.log
error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT

; 文件系统
open_basedir = /var/www/html:/tmp:/var/log/php
upload_tmp_dir = /var/www/tmp
session.save_path = /var/www/sessions

; 远程访问
allow_url_fopen = Off
allow_url_include = Off

; 危险函数
disable_functions = exec,passthru,shell_exec,system,proc_open,proc_close,proc_get_status,proc_nice,proc_terminate,popen,pclose,pcntl_exec,pcntl_fork,pcntl_signal,pcntl_alarm,pcntl_waitpid,posix_kill,posix_mkfifo,posix_setpgid,posix_setsid,posix_setuid,posix_seteuid,posix_setgid,posix_setegid,dl,chown,chgrp,chmod,symlink,link,phpinfo,get_current_user,getmyuid,getmygid,getmyinode,getmypid,getrusage,php_uname,apache_child_terminate,apache_get_modules,apache_get_version,apache_getenv,apache_note,define_syslog_variables,eval,assert,create_function,ini_restore

; 资源限制
max_execution_time = 30
max_input_time = 60
memory_limit = 128M
max_input_vars = 1000
max_input_nesting_level = 64
post_max_size = 8M
upload_max_filesize = 2M
max_file_uploads = 20

; 会话
session.save_handler = files
session.save_path = "/var/www/sessions"
session.name = "SESSID"
session.use_only_cookies = 1
session.use_trans_sid = 0
session.use_strict_mode = 1
session.cookie_lifetime = 0
session.cookie_path = /
session.cookie_secure = 1
session.cookie_httponly = 1
session.cookie_samesite = "Strict"
session.gc_probability = 1
session.gc_divisor = 1000
session.gc_maxlifetime = 1440

; 上传
file_uploads = On
upload_tmp_dir = /var/www/tmp

7.2 开发环境配置模板

ini 复制代码
; =====================================================
; PHP开发环境配置模板
; 注意:此配置不适用于生产环境
; =====================================================

[PHP]
; 信息披露
expose_php = On
display_errors = On
display_startup_errors = On
log_errors = On
html_errors = On
error_log = /var/log/php/error.log
error_reporting = E_ALL

; 文件系统(开发环境可适当放宽)
open_basedir =

; 远程访问(根据需要开启)
allow_url_fopen = On
allow_url_include = Off

; 危险函数(开发环境不禁用,但应了解风险)
disable_functions =

; 资源限制(开发环境更宽松)
max_execution_time = 300
max_input_time = 300
memory_limit = 512M
max_input_vars = 3000
post_max_size = 50M
upload_max_filesize = 50M

; 会话(开发环境不需要严格安全)
session.cookie_secure = 0
session.cookie_httponly = 1
session.cookie_samesite = "Lax"

; 调试(安装Xdebug)
; zend_extension=xdebug.so
; xdebug.mode=debug
; xdebug.start_with_request=yes

7.3 Docker/容器环境配置

ini 复制代码
; =====================================================
; PHP容器环境配置
; =====================================================

[PHP]
; 日志输出到stderr(容器最佳实践)
error_log = /proc/self/fd/2
log_errors = On
display_errors = Off

; 不写入文件,避免卷权限问题
; 使用外部日志收集器

; 其他配置与生产环境相同
; ...
dockerfile 复制代码
# Dockerfile安全配置示例
FROM php:8.1-fpm-alpine

# 复制自定义php.ini
COPY php-production.ini /usr/local/etc/php/php.ini

# 设置权限
RUN chmod 644 /usr/local/etc/php/php.ini

# 创建日志目录
RUN mkdir -p /var/log/php && chown www-data:www-data /var/log/php

# 以非root用户运行
USER www-data

8. 配置审计与监控

8.1 配置审计脚本

php 复制代码
<?php
/**
 * PHP安全配置审计工具
 */

class PhpSecurityAuditor {
    private $results = [];
    private $score = 0;
    private $maxScore = 0;

    public function runAudit(): array {
        $this->checkCriticalSettings();
        $this->checkInformationDisclosure();
        $this->checkResourceLimits();
        $this->checkSessionSecurity();
        $this->checkFileUploads();
        $this->checkDangerousFeatures();

        return [
            'score' => $this->score,
            'max_score' => $this->maxScore,
            'percentage' => round(($this->score / $this->maxScore) * 100, 2),
            'results' => $this->results
        ];
    }

    private function check(string $name, callable $check, int $points = 1): void {
        $this->maxScore += $points;

        try {
            $passed = $check();
            $this->results[$name] = [
                'status' => $passed ? 'PASS' : 'FAIL',
                'points' => $passed ? $points : 0
            ];
            if ($passed) {
                $this->score += $points;
            }
        } catch (Exception $e) {
            $this->results[$name] = [
                'status' => 'ERROR',
                'message' => $e->getMessage(),
                'points' => 0
            ];
        }
    }

    private function checkCriticalSettings(): void {
        $this->check('expose_php_off', fn() => ini_get('expose_php') === '');
        $this->check('display_errors_off', fn() => ini_get('display_errors') === '');
        $this->check('allow_url_include_off', fn() => ini_get('allow_url_include') === '');
        $this->check('disable_functions_set', fn() => !empty(ini_get('disable_functions')), 2);
        $this->check('open_basedir_set', fn() => !empty(ini_get('open_basedir')), 2);
    }

    private function checkInformationDisclosure(): void {
        $this->check('log_errors_on', fn() => ini_get('log_errors') === '1');
        $this->check('html_errors_off', fn() => ini_get('html_errors') === '');
        $this->check('error_reporting_appropriate', function() {
            $level = ini_get('error_reporting');
            return $level === '' || (int)$level <= (E_ALL & ~E_DEPRECATED & ~E_STRICT);
        });
    }

    private function checkResourceLimits(): void {
        $this->check('memory_limit_set', fn() => ini_get('memory_limit') !== '-1' && ini_get('memory_limit') !== '');
        $this->check('max_execution_time_set', fn() => (int)ini_get('max_execution_time') > 0);
        $this->check('max_input_vars_set', fn() => (int)ini_get('max_input_vars') < 10000);
    }

    private function checkSessionSecurity(): void {
        $this->check('session_cookie_httponly', fn() => ini_get('session.cookie_httponly') === '1');
        $this->check('session_cookie_secure', fn() => ini_get('session.cookie_secure') === '1');
        $this->check('session_use_strict_mode', fn() => ini_get('session.use_strict_mode') === '1');
        $this->check('session_use_only_cookies', fn() => ini_get('session.use_only_cookies') === '1');
    }

    private function checkFileUploads(): void {
        $this->check('upload_max_filesize_reasonable', fn() => $this->parseSize(ini_get('upload_max_filesize')) <= 10 * 1024 * 1024);
        $this->check('post_max_size_reasonable', fn() => $this->parseSize(ini_get('post_max_size')) <= 50 * 1024 * 1024);
    }

    private function checkDangerousFeatures(): void {
        $this->check('register_globals_off', fn() => ini_get('register_globals') !== '1');
        $this->check('magic_quotes_off', fn() => ini_get('magic_quotes_gpc') !== '1');
        $this->check('allow_url_fopen_off', fn() => ini_get('allow_url_fopen') === '', 2);
    }

    private function parseSize(string $size): int {
        $unit = strtolower(substr($size, -1));
        $value = (int) $size;

        return match($unit) {
            'g' => $value * 1024 * 1024 * 1024,
            'm' => $value * 1024 * 1024,
            'k' => $value * 1024,
            default => $value
        };
    }

    public function generateReport(): string {
        $audit = $this->runAudit();

        $report = "PHP Security Configuration Audit Report\n";
        $report .= str_repeat('=', 50) . "\n\n";
        $report .= "Score: {$audit['score']}/{$audit['max_score']} ({$audit['percentage']}%)\n\n";

        foreach ($audit['results'] as $check => $result) {
            $icon = $result['status'] === 'PASS' ? '✓' : ($result['status'] === 'FAIL' ? '✗' : '?');
            $report .= "$icon $check: {$result['status']}\n";
        }

        return $report;
    }
}

// 运行审计
$auditor = new PhpSecurityAuditor();
echo $auditor->generateReport();
?>

8.2 配置变更监控

php 复制代码
<?php
/**
 * 配置变更监控
 */

class ConfigMonitor {
    private $baselineFile;

    public function __construct(string $baselineFile = '/var/www/config/.baseline') {
        $this->baselineFile = $baselineFile;
    }

    /**
     * 创建配置基线
     */
    public function createBaseline(): void {
        $config = $this->getCriticalConfig();
        file_put_contents(
            $this->baselineFile,
            json_encode($config, JSON_PRETTY_PRINT)
        );
        chmod($this->baselineFile, 0600);
    }

    /**
     * 检查配置变更
     */
    public function checkChanges(): array {
        if (!file_exists($this->baselineFile)) {
            throw new RuntimeException("Baseline not found");
        }

        $baseline = json_decode(file_get_contents($this->baselineFile), true);
        $current = $this->getCriticalConfig();

        $changes = [];

        foreach ($current as $key => $value) {
            if (!isset($baseline[$key])) {
                $changes[] = [
                    'setting' => $key,
                    'type' => 'added',
                    'current' => $value
                ];
            } elseif ($baseline[$key] !== $value) {
                $changes[] = [
                    'setting' => $key,
                    'type' => 'modified',
                    'baseline' => $baseline[$key],
                    'current' => $value
                ];
            }
        }

        foreach ($baseline as $key => $value) {
            if (!isset($current[$key])) {
                $changes[] = [
                    'setting' => $key,
                    'type' => 'removed',
                    'baseline' => $value
                ];
            }
        }

        return $changes;
    }

    private function getCriticalConfig(): array {
        $settings = [
            'expose_php',
            'display_errors',
            'display_startup_errors',
            'log_errors',
            'error_reporting',
            'allow_url_fopen',
            'allow_url_include',
            'disable_functions',
            'open_basedir',
            'memory_limit',
            'max_execution_time',
            'max_input_vars',
            'file_uploads',
            'upload_max_filesize',
            'post_max_size',
            'session.cookie_httponly',
            'session.cookie_secure',
            'session.use_strict_mode',
            'session.use_only_cookies',
        ];

        $config = [];
        foreach ($settings as $setting) {
            $config[$setting] = ini_get($setting);
        }

        return $config;
    }
}
?>

9. 总结与延伸

9.1 关键要点回顾

  1. 分层防御

    • php.ini是第一道防线,但不是唯一防线
    • 结合代码层安全措施使用
    • 定期审计配置变更
  2. 最小权限原则

    • 禁用不必要的函数
    • 限制文件系统访问范围
    • 合理设置资源限制
  3. 信息披露控制

    • 生产环境关闭错误显示
    • 隐藏PHP版本信息
    • 安全存储日志
  4. 会话安全

    • 使用HttpOnly、Secure、SameSite
    • 启用严格模式
    • 定期清理过期会话
  5. 场景化配置

    • 开发、测试、生产环境使用不同配置
    • 容器环境考虑特殊性
    • 定期更新配置模板

9.2 常见问题速答

Q1: disable_functions 能完全禁用函数吗?

部分函数如 eval 是语言结构,不能被完全禁用。disable_functions 会在调用时产生警告,但代码仍可执行。真正的防护需要在代码审计阶段排除这些调用。

Q2: open_basedir 能被绕过吗?

是的,存在多种绕过方式:

  • 使用 /proc/self/root/ 等路径
  • 利用符号链接
  • 通过 chdir() 切换目录

应将其作为深度防御的一部分,而非唯一防线。

Q3: 如何在不修改php.ini的情况下设置配置?

php 复制代码
<?php
// 运行时设置(仅对可修改的指令有效)
ini_set('display_errors', '0');
ini_set('memory_limit', '256M');

// PHP-FPM池配置
; php_admin_value[memory_limit] = 128M
; php_admin_flag[display_errors] = off

// .htaccess (如果AllowOverride Options启用)
; php_flag display_errors off
; php_value memory_limit 128M
?>

Q4: 如何验证配置是否生效?

php 复制代码
<?php
// 方法1: 直接检查
if (ini_get('display_errors')) {
    echo "Warning: display_errors is enabled\n";
}

// 方法2: 使用phpinfo()(开发环境)
// phpinfo(INFO_CONFIGURATION);

// 方法3: 加载测试
$functions = explode(',', ini_get('disable_functions'));
if (!in_array('exec', $functions)) {
    echo "Warning: exec is not disabled\n";
}
?>
相关推荐
JSON_L2 小时前
Fastadmin中使用GatewayClient
php·fastadmin
青茶3602 小时前
php怎么实现订单接口状态轮询请求
前端·javascript·php
wxin_VXbishe4 小时前
C#(asp.net)学员竞赛信息管理系统-计算机毕业设计源码28790
java·vue.js·spring boot·spring·django·c#·php
迎仔5 小时前
11-云网络与混合云运维:弹性数字世界的交通管理
网络·安全·web安全
pitch_dark5 小时前
渗透测试系统基础篇——kali系统
网络·安全·web安全
世界尽头与你5 小时前
(修复方案)基础目录枚举漏洞
安全·网络安全·渗透测试
ん贤5 小时前
一次批量删除引发的死锁,最终我选择不加锁
数据库·安全·go·死锁
独行soc5 小时前
2026年渗透测试面试题总结-20(题目+回答)
android·网络·安全·web安全·渗透测试·安全狮
翼龙云_cloud6 小时前
阿里云渠道商:阿里云 ECS 从安全组到云防火墙的实战防护指南
安全·阿里云·云计算