1.修复上传漏洞和SQL注入漏洞

This commit is contained in:
2026-06-24 09:59:58 +08:00
parent 8c5b1d212a
commit aa5bda04ad
9 changed files with 147 additions and 45 deletions

View File

@@ -17,7 +17,6 @@ use support\Response;
class Ajax extends Backend
{
protected array $noNeedPermission = ['*'];
protected array $noNeedLogin = ['terminal'];
public function upload(Request $request): Response
{

View File

@@ -131,12 +131,23 @@ class Module extends Backend
if ($response !== null) return $response;
AdminLog::instance($request)->setTitle(__('Upload module'));
$file = $request->file('file');
if (!$file) {
return $this->error(__('Parameter error'));
$token = $request->post('token', $request->get('token', ''));
if (!$token) {
return $this->error(__('Please login to the official website account first'));
}
try {
$res = Manage::uploadFromRequest($request);
if ($request->file('file')) {
$res = Manage::uploadFromRequest($request);
} else {
$file = $request->post('file', $request->get('file', ''));
if (!$file) {
return $this->error(__('Parameter error'));
}
$info = Manage::instance('')->doUpload($token, $file);
$res = ['info' => $info];
}
} catch (BaException $e) {
return $this->error(__($e->getMessage()), $e->getData(), $e->getCode());
} catch (\Throwable $e) {

View File

@@ -30,7 +30,6 @@ class Crud extends Backend
protected string $webTranslate = '';
protected array $langTsData = [];
protected array $dtStringToArray = ['checkbox', 'selects', 'remoteSelects', 'city', 'images', 'files'];
protected array $noNeedLogin = ['getFileData'];
protected array $noNeedPermission = ['logStart', 'getFileData', 'parseFieldData', 'generateCheck', 'uploadCompleted'];
protected function initController(Request $request): ?Response

View File

@@ -95,7 +95,12 @@ class Manage
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
$saveName = 'temp' . DIRECTORY_SEPARATOR . date('YmdHis') . '_' . ($file->getUploadName() ?? 'module.zip');
$originalName = $file->getUploadName() ?? 'module.zip';
$baseName = basename(str_replace(['\\', '/'], DIRECTORY_SEPARATOR, $originalName));
if (!preg_match('/^[a-zA-Z0-9._-]+\.zip$/i', $baseName)) {
throw new Exception('The uploaded file format is not allowed');
}
$saveName = 'temp' . DIRECTORY_SEPARATOR . date('YmdHis') . '_' . $baseName;
$savePath = $uploadDir . $saveName;
$saveDir = dirname($savePath);
if (!is_dir($saveDir)) {
@@ -155,10 +160,7 @@ class Manage
*/
public function doUpload(string $token, string $file): array
{
$file = Filesystem::fsFit(root_path() . 'public' . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $file));
if (!is_file($file)) {
throw new Exception('Zip file not found');
}
$file = self::resolvePublicStorageFile($file);
$copyTo = $this->installDir . 'uploadTemp' . date('YmdHis') . '.zip';
copy($file, $copyTo);
@@ -897,4 +899,52 @@ class Manage
$this->modulesDir = $this->installDir . $uid . DIRECTORY_SEPARATOR;
return $this;
}
/**
* 解析并校验模块包路径,仅允许 public/storage 下的 zip 文件,防止路径穿越
* @throws Exception
*/
protected static function resolvePublicStorageFile(string $file): string
{
if (preg_match('#^(https?:)?//#i', $file)) {
$parsed = parse_url($file);
$file = $parsed['path'] ?? '';
}
$relative = ltrim(str_replace('\\', '/', $file), '/');
if ($relative === '' || str_contains($relative, '..')) {
throw new Exception('Invalid file path');
}
$publicRoot = realpath(public_path());
if ($publicRoot === false) {
throw new Exception('Invalid file path');
}
$candidate = Filesystem::fsFit($publicRoot . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $relative));
$fullPath = realpath($candidate);
if ($fullPath === false || !is_file($fullPath)) {
throw new Exception('Zip file not found');
}
$publicPrefix = rtrim($publicRoot, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
if (!str_starts_with($fullPath, $publicPrefix)) {
throw new Exception('Invalid file path');
}
$storageRoot = realpath(public_path('storage'));
if ($storageRoot === false) {
throw new Exception('Invalid file path');
}
$storagePrefix = rtrim($storageRoot, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
if (!str_starts_with($fullPath, $storagePrefix)) {
throw new Exception('Invalid file path');
}
if (strtolower(pathinfo($fullPath, PATHINFO_EXTENSION)) !== 'zip') {
throw new Exception('The uploaded file format is not allowed');
}
return $fullPath;
}
}

View File

@@ -389,6 +389,9 @@ class Install extends Api
public function testDatabase(Request $request): Response
{
$this->setRequest($request);
if ($this->isInstallComplete()) {
return $this->error(__('The system has completed installation. If you need to reinstall, please delete the %file% file first', ['%file%' => 'public/' . self::$lockFileName]));
}
$database = [
'hostname' => $request->post('hostname'),
'username' => $request->post('username'),
@@ -571,13 +574,7 @@ class Install extends Api
protected function isInstallComplete(): bool
{
if (is_file(public_path(self::$lockFileName))) {
$contents = @file_get_contents(public_path(self::$lockFileName));
if ($contents == self::$InstallationCompletionMark) {
return true;
}
}
return false;
return is_system_installed();
}
/**
@@ -656,6 +653,9 @@ class Install extends Api
public function accessUrls(Request $request): Response
{
$this->setRequest($request);
if ($this->isInstallComplete()) {
return $this->error(__('The system has completed installation. If you need to reinstall, please delete the %file% file first', ['%file%' => 'public/' . self::$lockFileName]));
}
$host = $request->header('host', '127.0.0.1:8787');
$port = '8787';
if (str_contains($host, ':')) {
@@ -684,6 +684,9 @@ class Install extends Api
public function manualInstall(Request $request): Response
{
$this->setRequest($request);
if ($this->isInstallComplete()) {
return $this->error(__('The system has completed installation. If you need to reinstall, please delete the %file% file first', ['%file%' => 'public/' . self::$lockFileName]));
}
return $this->success('', [
'webPath' => str_replace('\\', '/', root_path() . 'web')
]);
@@ -692,6 +695,9 @@ class Install extends Api
public function mvDist(Request $request): Response
{
$this->setRequest($request);
if ($this->isInstallComplete()) {
return $this->error(__('The system has completed installation. If you need to reinstall, please delete the %file% file first', ['%file%' => 'public/' . self::$lockFileName]));
}
if (!is_file(root_path() . self::$distDir . DIRECTORY_SEPARATOR . 'index.html')) {
return $this->error(__('No built front-end file found, please rebuild manually!'));
}

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace app\common\middleware;
use Webman\Http\Request;
use Webman\Http\Response;
use Webman\MiddlewareInterface;
/**
* 已安装系统禁止访问安装 API
*/
class InstallGuard implements MiddlewareInterface
{
public function process(Request $request, callable $handler): Response
{
if (is_system_installed()) {
return new Response(403, ['Content-Type' => 'application/json'], json_encode([
'code' => 0,
'msg' => __('The system has completed installation. If you need to reinstall, please delete the %file% file first', ['%file%' => 'public/install.lock']),
'time' => time(),
'data' => null,
], JSON_UNESCAPED_UNICODE));
}
return $handler($request);
}
}

View File

@@ -646,6 +646,17 @@ if (!function_exists('get_account_verification_type')) {
}
}
if (!function_exists('is_system_installed')) {
/**
* 系统是否已完成安装install.lock 内容为 install-end
*/
function is_system_installed(): bool
{
$lockFile = public_path('install.lock');
return is_file($lockFile) && @file_get_contents($lockFile) === 'install-end';
}
}
if (!function_exists('get_area')) {
function get_area($request = null): array
{