mirror of
https://github.com/crmeb/CRMEB.git
synced 2026-10-29 05:50:50 +00:00
【程序目录】更新
This commit is contained in:
parent
0a5aa8bce3
commit
bb0f66b2d8
@ -27,6 +27,16 @@ class SystemAttachment extends AuthController
|
||||
*/
|
||||
protected $service;
|
||||
|
||||
/**
|
||||
* 每分钟最大上传次数
|
||||
*/
|
||||
const UPLOAD_PER_MINUTE = 60;
|
||||
|
||||
/**
|
||||
* 每小时最大上传次数
|
||||
*/
|
||||
const UPLOAD_PER_HOUR = 500;
|
||||
|
||||
/**
|
||||
* @param App $app
|
||||
* @param SystemAttachmentServices $service
|
||||
@ -37,6 +47,41 @@ class SystemAttachment extends AuthController
|
||||
$this->service = $service;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查上传速率限制
|
||||
* @return bool
|
||||
*/
|
||||
protected function checkUploadRateLimit()
|
||||
{
|
||||
$adminId = $this->adminId;
|
||||
$minuteKey = 'admin_upload_minute_' . $adminId;
|
||||
$hourKey = 'admin_upload_hour_' . $adminId;
|
||||
|
||||
if (CacheService::has($minuteKey) && CacheService::get($minuteKey) >= self::UPLOAD_PER_MINUTE) {
|
||||
return false;
|
||||
}
|
||||
if (CacheService::has($hourKey) && CacheService::get($hourKey) >= self::UPLOAD_PER_HOUR) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新上传速率计数
|
||||
*/
|
||||
protected function incUploadRateLimit()
|
||||
{
|
||||
$adminId = $this->adminId;
|
||||
$minuteKey = 'admin_upload_minute_' . $adminId;
|
||||
$hourKey = 'admin_upload_hour_' . $adminId;
|
||||
|
||||
$minuteCount = CacheService::has($minuteKey) ? (int)CacheService::get($minuteKey) : 0;
|
||||
$hourCount = CacheService::has($hourKey) ? (int)CacheService::get($hourKey) : 0;
|
||||
|
||||
CacheService::set($minuteKey, $minuteCount + 1, 60);
|
||||
CacheService::set($hourKey, $hourCount + 1, 3600);
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示列表
|
||||
* @return mixed
|
||||
@ -72,12 +117,16 @@ class SystemAttachment extends AuthController
|
||||
*/
|
||||
public function upload($upload_type = 0, $type = 0)
|
||||
{
|
||||
if (!$this->checkUploadRateLimit()) {
|
||||
return app('json')->fail('上传过于频繁,请稍后再试');
|
||||
}
|
||||
[$pid, $file, $menuName] = $this->request->postMore([
|
||||
['pid', 0],
|
||||
['file', 'file'],
|
||||
['menu_name', '']
|
||||
], true);
|
||||
$res = $this->service->upload((int)$pid, $file, $upload_type, $type, $menuName);
|
||||
$this->incUploadRateLimit();
|
||||
return app('json')->success('上传成功', ['src' => $res]);
|
||||
}
|
||||
|
||||
@ -126,6 +175,9 @@ class SystemAttachment extends AuthController
|
||||
*/
|
||||
public function videoUpload()
|
||||
{
|
||||
if (!$this->checkUploadRateLimit()) {
|
||||
return app('json')->fail('上传过于频繁,请稍后再试');
|
||||
}
|
||||
$data = $this->request->postMore([
|
||||
['chunkNumber', 0],//第几分片
|
||||
['currentChunkSize', 0],//分片大小
|
||||
@ -136,6 +188,7 @@ class SystemAttachment extends AuthController
|
||||
['filename', ''],//文件名称
|
||||
]);
|
||||
$res = $this->service->videoUpload($data, $_FILES['file']);
|
||||
$this->incUploadRateLimit();
|
||||
return app('json')->success($res);
|
||||
}
|
||||
|
||||
|
||||
@ -614,10 +614,31 @@ if (!function_exists('put_image')) {
|
||||
$filename = time() . "." . $ext;
|
||||
}
|
||||
|
||||
// SSRF 防护:验证 URL 安全性
|
||||
$urlParts = parse_url($url);
|
||||
if (!$urlParts || !isset($urlParts['scheme']) || !in_array($urlParts['scheme'], ['http', 'https'])) {
|
||||
return false;
|
||||
}
|
||||
$host = $urlParts['host'] ?? '';
|
||||
if (!$host) {
|
||||
return false;
|
||||
}
|
||||
// 阻止访问内网地址和私有 IP
|
||||
$ip = gethostbyname($host);
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 保存文件到指定目录
|
||||
$imgData = file_get_contents($url);
|
||||
if ($imgData === false) {
|
||||
return false;
|
||||
}
|
||||
$pattern = '/<\?php(.*?)\?>/s';
|
||||
$imgData = preg_replace($pattern, '', $imgData);
|
||||
// 额外过滤短标签
|
||||
$imgData = preg_replace('/<\?=(.*?)\?>/s', '', $imgData);
|
||||
$imgData = preg_replace('/<\?[\s](.*?)\?>/s', '', $imgData);
|
||||
if ($imgData !== false) {
|
||||
$path = 'uploads' . DS . 'qrcode' . DS . $filename;
|
||||
if (file_put_contents($path, $imgData) !== false) {
|
||||
|
||||
@ -213,19 +213,32 @@ class Common extends BaseController
|
||||
$data = $request->postMore([
|
||||
['filename', 'file'],
|
||||
]);
|
||||
if (!$data['filename']) return app('json')->fail('参数错误');
|
||||
|
||||
// 尝试解析 token 获取用户身份
|
||||
$uid = 0;
|
||||
try {
|
||||
/** @var UserAuthServices $service */
|
||||
$service = app()->make(UserAuthServices::class);
|
||||
$authInfo = $service->parseToken($this->request->post('token'));
|
||||
if (!isset($authInfo['user']['uid'])) {
|
||||
return app('json')->fail('非法操作');
|
||||
if (isset($authInfo['user']['uid']) && $authInfo['user']['uid'] > 0) {
|
||||
$uid = (int)$authInfo['user']['uid'];
|
||||
}
|
||||
} catch (AuthException $e) {
|
||||
return app('json')->fail('无效的token不能查找到用户聊天记录');
|
||||
} catch (\Throwable $e) {
|
||||
// token 无效时使用 IP 限流
|
||||
}
|
||||
$uid = $authInfo['user']['uid'];
|
||||
if (!$data['filename']) return app('json')->fail('参数错误');
|
||||
if (CacheService::has('start_uploads_' . $uid) && CacheService::get('start_uploads_' . $uid) >= 100) return app('json')->fail('非法操作');
|
||||
|
||||
// 速率限制:认证用户 100次/天,未认证 IP 50次/天
|
||||
if ($uid > 0) {
|
||||
$limitKey = 'start_uploads_' . $uid;
|
||||
$maxUploads = 100;
|
||||
} else {
|
||||
$limitKey = 'start_uploads_ip_' . md5($request->ip());
|
||||
$maxUploads = 50;
|
||||
}
|
||||
|
||||
if (CacheService::has($limitKey) && CacheService::get($limitKey) >= $maxUploads) return app('json')->fail('上传次数已达上限');
|
||||
|
||||
$upload = UploadService::init();
|
||||
$info = $upload->to('store/comment')->validate()->move($data['filename']);
|
||||
if ($info === false) {
|
||||
@ -233,12 +246,15 @@ class Common extends BaseController
|
||||
}
|
||||
$res = $upload->getUploadInfo();
|
||||
$services->attachmentAdd($res['name'], $res['size'], $res['type'], $res['dir'], $res['thumb_path'], 1, (int)sys_config('upload_type', 1), $res['time'], 2);
|
||||
if (CacheService::has('start_uploads_' . $uid))
|
||||
$start_uploads = (int)CacheService::get('start_uploads_' . $uid);
|
||||
|
||||
// 更新速率计数
|
||||
if (CacheService::has($limitKey))
|
||||
$start_uploads = (int)CacheService::get($limitKey);
|
||||
else
|
||||
$start_uploads = 0;
|
||||
$start_uploads++;
|
||||
CacheService::set('start_uploads_' . $uid, $start_uploads, 86400);
|
||||
CacheService::set($limitKey, $start_uploads, 86400);
|
||||
|
||||
$res['dir'] = path_to_url($res['dir']);
|
||||
if (strpos($res['dir'], 'http') === false) $res['dir'] = $request->domain() . $res['dir'];
|
||||
return app('json')->success('图片上传成功', ['name' => $res['name'], 'url' => $res['dir']]);
|
||||
|
||||
@ -254,16 +254,27 @@ class SystemAttachmentServices extends BaseServices
|
||||
*/
|
||||
public function videoUpload($data, $file)
|
||||
{
|
||||
$pathinfo = pathinfo($data['filename']);
|
||||
if (isset($pathinfo['extension']) && !in_array($pathinfo['extension'], ['avi', 'mp4', 'wmv', 'rm', 'mpg', 'mpeg', 'mov', 'flv', 'swf'])) {
|
||||
// 安全修复:使用 basename 防止路径穿越攻击
|
||||
$safeFilename = basename($data['filename']);
|
||||
if ($safeFilename !== $data['filename']) {
|
||||
throw new AdminException('文件名包含非法路径字符');
|
||||
}
|
||||
|
||||
$pathinfo = pathinfo($safeFilename);
|
||||
if (!isset($pathinfo['extension']) || !in_array($pathinfo['extension'], ['avi', 'mp4', 'wmv', 'rm', 'mpg', 'mpeg', 'mov', 'flv', 'swf'])) {
|
||||
throw new AdminException('格式错误');
|
||||
}
|
||||
// 危险后缀黑名单,防止上传可执行脚本
|
||||
$dangerousExt = ['php', 'phtml', 'pht', 'php3', 'php4', 'php5', 'php7', 'shtml', 'shtm', 'htaccess', 'cgi', 'pl', 'py', 'jsp', 'asp', 'aspx'];
|
||||
if (in_array($pathinfo['extension'], $dangerousExt)) {
|
||||
throw new AdminException('不支持的文件格式');
|
||||
}
|
||||
$data['chunkNumber'] = (int)$data['chunkNumber'];
|
||||
$public_dir = app()->getRootPath() . 'public';
|
||||
$dir = '/uploads/attach/' . date('Y') . DIRECTORY_SEPARATOR . date('m') . DIRECTORY_SEPARATOR . date('d');
|
||||
$all_dir = $public_dir . $dir;
|
||||
if (!is_dir($all_dir)) mkdir($all_dir, 0777, true);
|
||||
$filename = $all_dir . '/' . $data['filename'] . '__' . $data['chunkNumber'];
|
||||
$filename = $all_dir . '/' . $safeFilename . '__' . $data['chunkNumber'];
|
||||
move_uploaded_file($file['tmp_name'], $filename);
|
||||
$res['code'] = 0;
|
||||
$res['msg'] = 'error';
|
||||
@ -271,19 +282,19 @@ class SystemAttachmentServices extends BaseServices
|
||||
if ($data['chunkNumber'] == $data['totalChunks']) {
|
||||
$blob = '';
|
||||
for ($i = 1; $i <= $data['totalChunks']; $i++) {
|
||||
$blob .= file_get_contents($all_dir . '/' . $data['filename'] . '__' . $i);
|
||||
$blob .= file_get_contents($all_dir . '/' . $safeFilename . '__' . $i);
|
||||
}
|
||||
file_put_contents($all_dir . '/' . $data['filename'], $blob);
|
||||
file_put_contents($all_dir . '/' . $safeFilename, $blob);
|
||||
for ($i = 1; $i <= $data['totalChunks']; $i++) {
|
||||
@unlink($all_dir . '/' . $data['filename'] . '__' . $i);
|
||||
@unlink($all_dir . '/' . $safeFilename . '__' . $i);
|
||||
}
|
||||
if (file_exists($all_dir . '/' . $data['filename'])) {
|
||||
if (file_exists($all_dir . '/' . $safeFilename)) {
|
||||
$res['code'] = 2;
|
||||
$res['msg'] = 'success';
|
||||
$res['file_path'] = sys_config('site_url') . $dir . '/' . $data['filename'];
|
||||
$res['file_path'] = sys_config('site_url') . $dir . '/' . $safeFilename;
|
||||
}
|
||||
} else {
|
||||
if (file_exists($all_dir . '/' . $data['filename'] . '__' . $data['chunkNumber'])) {
|
||||
if (file_exists($all_dir . '/' . $safeFilename . '__' . $data['chunkNumber'])) {
|
||||
$res['code'] = 1;
|
||||
$res['msg'] = 'waiting';
|
||||
$res['file_path'] = '';
|
||||
|
||||
@ -391,11 +391,51 @@ abstract class BaseUpload extends BaseStorage
|
||||
if (is_resource($stream)) {
|
||||
fclose($stream);
|
||||
}
|
||||
if (preg_match('/think|app|php|log|phar|Socket|Channel|Flysystem|Psr6Cache|Cached|Request|debug|Psr6Cachepool|eval/i', $content)) {
|
||||
// 检测 PHP 代码标签
|
||||
if (preg_match('/<\?php|<\?=|<\?[\s]/i', $content)) {
|
||||
return $this->setError('文件内容包含非法代码');
|
||||
}
|
||||
// 检测危险函数调用
|
||||
if (preg_match('/\beval\s*\(|\bsystem\s*\(|\bexec\s*\(|\bshell_exec\s*\(|\bpassthru\s*\(|\bassert\s*\(|\bcreate_function\s*\(|\bpopen\s*\(|\bproc_open\s*\(|\bpcntl_exec\s*\(/i', $content)) {
|
||||
return $this->setError('文件内容包含非法代码');
|
||||
}
|
||||
// 检测框架敏感关键词
|
||||
if (preg_match('/think|phar|Socket|Channel|Flysystem|Psr6Cache|Cached|Request|debug|Psr6Cachepool/i', $content)) {
|
||||
return $this->setError('文件内容不合法');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测文件内容是否包含脚本代码(字符串版本,用于 stream/down)
|
||||
* @param string $content
|
||||
* @return bool
|
||||
*/
|
||||
protected function checkContentSafety(string $content): bool
|
||||
{
|
||||
// 检测 PHP 代码标签
|
||||
if (preg_match('/<\?php|<\?=|<\?[\s]/i', $content)) {
|
||||
return false;
|
||||
}
|
||||
// 检测危险函数调用
|
||||
if (preg_match('/\beval\s*\(|\bsystem\s*\(|\bexec\s*\(|\bshell_exec\s*\(|\bpassthru\s*\(|\bassert\s*\(/i', $content)) {
|
||||
return false;
|
||||
}
|
||||
// 检测框架敏感关键词
|
||||
if (preg_match('/\bthink\b|\bphar\b|Socket|Channel|Psr6Cache|Cached|Psr6Cachepool/i', $content)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 危险文件扩展名黑名单
|
||||
* @var string[]
|
||||
*/
|
||||
protected $dangerousExtensions = [
|
||||
'php', 'phtml', 'pht', 'php3', 'php4', 'php5', 'php7', 'php8',
|
||||
'shtml', 'shtm', 'htaccess', 'cgi', 'pl', 'py', 'jsp', 'asp', 'aspx',
|
||||
];
|
||||
|
||||
/**
|
||||
* 文件上传
|
||||
* @return mixed
|
||||
|
||||
@ -125,6 +125,14 @@ class Local extends BaseUpload
|
||||
if (!in_array($fileHandle->getOriginalMime(), $this->validate['fileMime'])) {
|
||||
return $this->setError('不合法的文件类型');
|
||||
}
|
||||
// 危险后缀黑名单检查
|
||||
if (in_array(strtolower(pathinfo($fileHandle->getOriginalName(), PATHINFO_EXTENSION)), $this->dangerousExtensions)) {
|
||||
return $this->setError('不支持的文件格式');
|
||||
}
|
||||
// 对所有上传文件进行内容安全检测
|
||||
if ($this->checkFileContent($fileHandle) === false) {
|
||||
return false;
|
||||
}
|
||||
if (in_array($fileHandle->getOriginalMime(), ['image/x-icon', 'image/png', 'image/gif', 'image/jpeg', 'image/jpg', 'image/webp'])) {
|
||||
$stream = fopen($fileHandle->getPathname(), 'r');
|
||||
$content = (fread($stream, filesize($fileHandle->getPathname())));
|
||||
@ -182,6 +190,20 @@ class Local extends BaseUpload
|
||||
if (!$key) {
|
||||
$key = $this->saveFileName();
|
||||
}
|
||||
// 扩展名安全验证
|
||||
$ext = strtolower(pathinfo($key, PATHINFO_EXTENSION));
|
||||
if (in_array($ext, $this->dangerousExtensions)) {
|
||||
return $this->setError('不支持的文件格式');
|
||||
}
|
||||
// 文件大小验证 (默认最大50MB)
|
||||
$maxSize = $this->validate['filesize'] ?? 52428800;
|
||||
if (strlen($fileContent) > $maxSize) {
|
||||
return $this->setError('文件过大');
|
||||
}
|
||||
// 文件内容安全检测
|
||||
if (!$this->checkContentSafety($fileContent)) {
|
||||
return $this->setError('文件内容包含非法代码');
|
||||
}
|
||||
$dir = $this->uploadDir($this->path);
|
||||
if (!$this->validDir($dir)) {
|
||||
return $this->setError('Failed to generate upload directory, please check the permission!');
|
||||
@ -214,6 +236,20 @@ class Local extends BaseUpload
|
||||
if (!$key) {
|
||||
$key = $this->saveFileName();
|
||||
}
|
||||
// 扩展名安全验证
|
||||
$ext = strtolower(pathinfo($key, PATHINFO_EXTENSION));
|
||||
if (in_array($ext, $this->dangerousExtensions)) {
|
||||
return $this->setError('不支持的文件格式');
|
||||
}
|
||||
// 文件大小验证
|
||||
$maxSize = $this->validate['filesize'] ?? 52428800;
|
||||
if (strlen($fileContent) > $maxSize) {
|
||||
return $this->setError('文件过大');
|
||||
}
|
||||
// 文件内容安全检测
|
||||
if (!$this->checkContentSafety($fileContent)) {
|
||||
return $this->setError('文件内容包含非法代码');
|
||||
}
|
||||
$dir = $this->uploadDir($this->path);
|
||||
if (!$this->validDir($dir)) {
|
||||
return $this->setError('Failed to generate upload directory, please check the permission!');
|
||||
|
||||
@ -88,6 +88,18 @@ class DownloadImage
|
||||
$url = 'http:' . $url;
|
||||
}
|
||||
$url = str_replace('https://', 'http://', $url);
|
||||
|
||||
// SSRF 防护:验证 URL 安全性
|
||||
$urlParts = parse_url($url);
|
||||
if (!$urlParts || !isset($urlParts['host'])) {
|
||||
throw new AdminException('无效的图片地址');
|
||||
}
|
||||
// 阻止访问内网地址和私有 IP
|
||||
$ip = gethostbyname($urlParts['host']);
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {
|
||||
throw new AdminException('不允许访问内网地址');
|
||||
}
|
||||
|
||||
if ($this->path == 'attach') {
|
||||
$date_dir = date('Y') . DIRECTORY_SEPARATOR . date('m') . DIRECTORY_SEPARATOR . date('d');
|
||||
$to_path = $this->path . '/' . $date_dir;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user