在PHP项目开发中,为了避免静态资源被直接访问导致的安全问题,通常会将图像等文件存放在Web根目录之外的非Web可访问目录中。此时需要通过PHP脚本读取并输出图像,同时必须做好输入验证和MIME类型校验,防止出现路径遍历、恶意文件泄露等安全风险。

非Web可访问目录的配置与权限
首先需要确认非Web可访问目录的路径和权限,假设我们将图像存放在/var/www/private_images/目录,该目录不在Web服务器的根目录(如/var/www/html/)下,Web用户(通常是www-data)需要有该目录的读取权限。
可以通过如下命令设置目录权限:
# 设置目录所有者为Web用户 chown -R www-data:www-data /var/www/private_images/ # 设置目录权限为755,文件权限为644 chmod 755 /var/www/private_images/ chmod 644 /var/www/private_images/*
输入参数验证
加载图像时通常需要通过GET参数传递图像文件名,第一步必须验证输入参数的合法性,防止路径遍历攻击。常见的验证规则包括:只允许文件名包含字母、数字、下划线、点和短横线,禁止包含路径分隔符。
输入验证的示例代码如下:
<?php
// 获取请求的文件名参数
$filename = $_GET['filename'] ?? '';
// 验证文件名合法性,禁止路径遍历
if (empty($filename)) {
http_response_code(400);
exit('文件名不能为空');
}
// 正则匹配只允许字母、数字、下划线、点、短横线
if (!preg_match('/^[a-zA-Z0-9_.-]+$/', $filename)) {
http_response_code(400);
exit('文件名格式不合法');
}
// 拼接完整文件路径,非Web可访问目录
$baseDir = '/var/www/private_images/';
$filePath = $baseDir . $filename;
// 验证文件是否存在且是真实文件
if (!file_exists($filePath) || !is_file($filePath)) {
http_response_code(404);
exit('文件不存在');
}
?>
MIME类型验证
仅验证文件名后缀是不够的,攻击者可能上传伪装成图像的恶意文件,因此需要通过MIME类型校验确认文件真实类型。PHP中可以通过finfo_file函数获取文件的真实MIME类型。
MIME类型验证的代码如下:
<?php
// 允许的图像MIME类型
$allowedMimeTypes = [
'image/jpeg',
'image/png',
'image/gif',
'image/webp'
];
// 创建finfo实例,检查文件MIME类型
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$fileMimeType = finfo_file($finfo, $filePath);
finfo_close($finfo);
// 校验MIME类型是否在允许列表中
if (!in_array($fileMimeType, $allowedMimeTypes)) {
http_response_code(403);
exit('文件类型不合法');
}
?>
安全输出图像
完成验证后,需要设置正确的响应头并输出文件内容,同时要注意禁用缓存或者设置合理的缓存策略,避免敏感图像被缓存到客户端。
完整的安全输出代码如下:
<?php
// 设置响应头,指定MIME类型和文件名
header('Content-Type: ' . $fileMimeType);
header('Content-Disposition: inline; filename="' . basename($filePath) . '"');
header('Content-Length: ' . filesize($filePath));
// 禁止缓存敏感图像
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Pragma: no-cache');
// 读取并输出文件内容
readfile($filePath);
exit;
?>
常见安全风险规避
- 禁止直接使用用户输入拼接文件路径,避免路径遍历攻击,如
../等字符必须过滤。 - 不要信任客户端传递的MIME类型,必须服务端通过
finfo_file等函数校验真实类型。 - 非Web可访问目录的权限不要设置为777,仅给Web用户必要的读取权限即可。
- 对输出的图像设置合理的缓存策略,避免敏感图像被客户端缓存泄露。
完整整合示例
将前面的验证和输出逻辑整合后的完整脚本如下:
<?php
// 输入参数获取与验证
$filename = $_GET['filename'] ?? '';
if (empty($filename)) {
http_response_code(400);
exit('文件名不能为空');
}
if (!preg_match('/^[a-zA-Z0-9_.-]+$/', $filename)) {
http_response_code(400);
exit('文件名格式不合法');
}
// 文件路径拼接与存在性校验
$baseDir = '/var/www/private_images/';
$filePath = $baseDir . $filename;
if (!file_exists($filePath) || !is_file($filePath)) {
http_response_code(404);
exit('文件不存在');
}
// MIME类型校验
$allowedMimeTypes = [
'image/jpeg',
'image/png',
'image/gif',
'image/webp'
];
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$fileMimeType = finfo_file($finfo, $filePath);
finfo_close($finfo);
if (!in_array($fileMimeType, $allowedMimeTypes)) {
http_response_code(403);
exit('文件类型不合法');
}
// 输出图像
header('Content-Type: ' . $fileMimeType);
header('Content-Disposition: inline; filename="' . basename($filePath) . '"');
header('Content-Length: ' . filesize($filePath));
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Pragma: no-cache');
readfile($filePath);
exit;
?>