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 关键要点回顾
-
分层防御
- php.ini是第一道防线,但不是唯一防线
- 结合代码层安全措施使用
- 定期审计配置变更
-
最小权限原则
- 禁用不必要的函数
- 限制文件系统访问范围
- 合理设置资源限制
-
信息披露控制
- 生产环境关闭错误显示
- 隐藏PHP版本信息
- 安全存储日志
-
会话安全
- 使用HttpOnly、Secure、SameSite
- 启用严格模式
- 定期清理过期会话
-
场景化配置
- 开发、测试、生产环境使用不同配置
- 容器环境考虑特殊性
- 定期更新配置模板
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";
}
?>