本文目录导读:

在 PHP 中实现上传文件的安全扫描,需要结合多种方法,以下是完整的安全扫描方案:
基础上传安全配置
<?php
class SecureFileUpload {
private $allowedTypes = [
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/gif' => 'gif',
'application/pdf' => 'pdf'
];
private $maxFileSize = 5242880; // 5MB
private $uploadDir = '/uploads/';
public function upload($file) {
// 1. 基础验证
if (!$this->validateFile($file)) {
return ['success' => false, 'error' => '文件验证失败'];
}
// 2. 安全检查
$scanResult = $this->scanFile($file['tmp_name']);
if (!$scanResult['safe']) {
return ['success' => false, 'error' => $scanResult['message']];
}
// 3. 生成唯一文件名
$filename = $this->generateUniqueName($file['name']);
// 4. 移动文件
$destination = $this->uploadDir . $filename;
if (move_uploaded_file($file['tmp_name'], $destination)) {
return ['success' => true, 'path' => $destination];
}
return ['success' => false, 'error' => '文件保存失败'];
}
private function validateFile($file) {
// 检查错误
if ($file['error'] !== UPLOAD_ERR_OK) {
return false;
}
// 检查大小
if ($file['size'] > $this->maxFileSize) {
return false;
}
// 检查MIME类型
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($file['tmp_name']);
// 检查MIME类型白名单
if (!array_key_exists($mimeType, $this->allowedTypes)) {
return false;
}
// 验证文件扩展名
$extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if ($this->allowedTypes[$mimeType] !== $extension) {
return false;
}
return true;
}
private function generateUniqueName($originalName) {
$extension = pathinfo($originalName, PATHINFO_EXTENSION);
return date('YmdHis') . '_' . bin2hex(random_bytes(8)) . '.' . $extension;
}
}
恶意代码扫描
<?php
class MalwareScanner {
public function scan($filePath) {
$content = file_get_contents($filePath);
// 危险函数列表
$dangerousFunctions = [
'eval', 'exec', 'system', 'passthru', 'shell_exec',
'popen', 'proc_open', 'pcntl_exec'
];
foreach ($dangerousFunctions as $func) {
if (preg_match("/\b" . $func . "\s*\(/i", $content)) {
return [
'safe' => false,
'message' => "发现危险函数: {$func}"
];
}
}
// 危险代码模式
$dangerousPatterns = [
'/<\?php/i', // PHP代码
'/\b(base64_decode|gzinflate)\b/i', // 编码函数
'/\b(assert|create_function)\b/i', // 执行函数
'/\b($_POST|$_GET|$_REQUEST)\b/i', // 超全局变量
'/\b(file_put_contents|fwrite|fopen)\b/i', // 文件操作
];
foreach ($dangerousPatterns as $pattern) {
if (preg_match($pattern, $content)) {
return [
'safe' => false,
'message' => "发现危险代码模式"
];
}
}
return ['safe' => true];
}
public function checkMagicNumbers($filePath) {
$fp = fopen($filePath, 'rb');
$content = fread($fp, 4096);
fclose($fp);
// 检查是否包含PHP代码
if (preg_match('/<\?php/i', $content)) {
return false;
}
// 检查是否包含Shebang
if (strpos($content, '#!/usr/bin/') === 0) {
return false;
}
return true;
}
}
图像文件深度扫描
<?php
class ImageScanner {
public function scanImage($filePath) {
// 尝试获取图片信息
$imageInfo = @getimagesize($filePath);
if ($imageInfo === false) {
return [
'safe' => false,
'message' => '不是有效的图片文件'
];
}
// 重新生成图片,去除潜在恶意代码
return $this->recreateImage($filePath, $imageInfo);
}
private function recreateImage($filePath, $imageInfo) {
$type = $imageInfo[2];
$width = $imageInfo[0];
$height = $imageInfo[1];
// 根据类型创建新图片
switch ($type) {
case IMAGETYPE_JPEG:
$image = imagecreatefromjpeg($filePath);
// 重新生成
$newImage = imagecreatetruecolor($width, $height);
imagecopy($newImage, $image, 0, 0, 0, 0, $width, $height);
imagejpeg($newImage, $filePath, 90);
break;
case IMAGETYPE_PNG:
$image = imagecreatefrompng($filePath);
$newImage = imagecreatetruecolor($width, $height);
imagecopy($newImage, $image, 0, 0, 0, 0, $width, $height);
imagepng($newImage, $filePath, 9);
break;
case IMAGETYPE_GIF:
$image = imagecreatefromgif($filePath);
$newImage = imagecreatetruecolor($width, $height);
imagecopy($newImage, $image, 0, 0, 0, 0, $width, $height);
imagegif($newImage, $filePath);
break;
default:
return ['safe' => false, 'message' => '不支持的图片类型'];
}
imagedestroy($image);
imagedestroy($newImage);
return ['safe' => true, 'message' => '图片扫描完成'];
}
public function checkExifData($filePath) {
// 读取并清除EXIF数据
if (function_exists('exif_read_data')) {
$exif = @exif_read_data($filePath);
if ($exif) {
// 可以在这里记录或清除EXIF数据
// 实际上PHP没有直接的函数清除EXIF,需要通过修改图片实现
return [
'safe' => true,
'has_exif' => true,
'data' => $exif
];
}
}
return ['safe' => true, 'has_exif' => false];
}
}
完整的上传扫描流程
<?php
class UploadScanner {
private $securityScanners = [];
public function __construct() {
$this->securityScanners = [
new MalwareScanner(),
new ImageScanner()
];
}
public function processUpload($file) {
try {
// 1. 基础验证
$fileValidator = new SecureFileUpload();
// 2. 恶意代码扫描
$malwareScanner = new MalwareScanner();
$scanResult = $malwareScanner->scan($file['tmp_name']);
if (!$scanResult['safe']) {
throw new Exception($scanResult['message']);
}
// 3. 文件类型特定扫描
$mimeType = mime_content_type($file['tmp_name']);
if (strpos($mimeType, 'image/') === 0) {
$imageScanner = new ImageScanner();
$imageResult = $imageScanner->scanImage($file['tmp_name']);
if (!$imageResult['safe']) {
throw new Exception($imageResult['message']);
}
}
// 4. 文本文件内容检查
if ($this->isTextFile($mimeType)) {
$textResult = $this->scanTextContent($file['tmp_name']);
if (!$textResult['safe']) {
throw new Exception($textResult['message']);
}
}
// 5. 病毒扫描(可选,需要ClamAV)
if (extension_loaded('clamav')) {
$virusResult = $this->scanWithClamAV($file['tmp_name']);
if (!$virusResult['safe']) {
throw new Exception('检测到病毒');
}
}
return ['safe' => true, 'message' => '文件扫描通过'];
} catch (Exception $e) {
return ['safe' => false, 'message' => $e->getMessage()];
}
}
private function isTextFile($mimeType) {
return in_array($mimeType, [
'text/plain', 'text/html', 'text/xml',
'application/json', 'application/xml'
]);
}
private function scanTextContent($filePath) {
$content = file_get_contents($filePath);
// 检查HTML注入
if (preg_match('/<script[^>]*>/i', $content)) {
return ['safe' => false, 'message' => '检测到HTML脚本注入'];
}
// 检查SQL注入
if (preg_match('/\b(union|select)\b.*\b(from|where)\b/i', $content)) {
return ['safe' => false, 'message' => '检测到SQL注入模式'];
}
// 检查路径遍历
if (strpos($content, '..') !== false) {
return ['safe' => false, 'message' => '检测到路径遍历'];
}
return ['safe' => true];
}
public function scanWithClamAV($filePath) {
exec('clamscan ' . escapeshellarg($filePath) . ' 2>&1', $output, $returnCode);
return [
'safe' => $returnCode === 0,
'message' => implode(' ', $output)
];
}
}
上传处理入口
<?php
error_reporting(0);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!isset($_FILES['file'])) {
die(json_encode(['success' => false, 'error' => '没有文件上传']));
}
$uploadScanner = new UploadScanner();
// 开始扫描
$scanResult = $uploadScanner->processUpload($_FILES['file']);
if ($scanResult['safe']) {
// 扫描通过,继续处理上传
$uploader = new SecureFileUpload();
$uploadResult = $uploader->upload($_FILES['file']);
echo json_encode($uploadResult);
} else {
echo json_encode([
'success' => false,
'error' => $scanResult['message']
]);
}
}
高级安全措施
<?php
// CSRF Token验证
// 文件上传前验证CSRF token
if (!isset($_POST['csrf_token']) || !validateCsrfToken($_POST['csrf_token'])) {
die('CSRF token验证失败');
}
// 限速上传
// 使用简单的速率限制
$ip = $_SERVER['REMOTE_ADDR'];
$key = 'upload_rate_' . $ip;
$limit = 5; // 5次上传
$window = 3600; // 1小时
if ($redis->get($key) >= $limit) {
die('上传过于频繁');
}
$redis->incr($key);
$redis->expire($key, $window);
// 双重验证:客户端和服务端
// 验证文件的哈希值
$fileHash = hash_file('sha256', $_FILES['file']['tmp_name']);
if (in_array($fileHash, $blockedHashes)) {
die('该文件已被标记为恶意文件');
}
错误处理和日志
<?php
class UploadLogger {
public static function log($message, $type = 'info') {
$logFile = '/logs/upload_' . date('Y-m-d') . '.log';
$message = date('[Y-m-d H:i:s]') . " [$type] " . $message . "\n";
file_put_contents($logFile, $message, FILE_APPEND);
}
public static function logSuspiciousActivity($fileInfo, $reason) {
self::log("可疑文件: {$fileInfo['name']} - IP: " . $_SERVER['REMOTE_ADDR'] . " - 原因: $reason", 'warning');
}
}
// 在扫描失败时记录
$uploadLogger = new UploadLogger();
$uploadLogger->logSuspiciousActivity($_FILES['file'], $scanResult['message']);
完整的上传扫描应该包括:
- 基础验证:文件大小、类型、扩展名
- 恶意代码扫描:检测PHP代码、危险函数安全扫描**:检测SQL注入、XSS、路径遍历
- 文件特定扫描:图片重生成、PDF检查
- 病毒扫描:集成ClamAV
- 日志记录:记录所有上传活动和可疑行为
建议在生产环境中使用多层安全防护,并定期更新扫描规则。