From aa5bda04adaaff43046dbfacb3441e8dcb547dc1 Mon Sep 17 00:00:00 2001 From: zhenhui <1276357500@qq.com> Date: Wed, 24 Jun 2026 09:59:58 +0800 Subject: [PATCH] =?UTF-8?q?1.=E4=BF=AE=E5=A4=8D=E4=B8=8A=E4=BC=A0=E6=BC=8F?= =?UTF-8?q?=E6=B4=9E=E5=92=8CSQL=E6=B3=A8=E5=85=A5=E6=BC=8F=E6=B4=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + app/admin/controller/Ajax.php | 1 - app/admin/controller/Module.php | 19 ++++++-- app/admin/controller/crud/Crud.php | 1 - app/admin/library/module/Manage.php | 60 +++++++++++++++++++++++--- app/api/controller/Install.php | 20 ++++++--- app/common/middleware/InstallGuard.php | 29 +++++++++++++ app/functions.php | 11 +++++ config/route.php | 50 ++++++++++----------- 9 files changed, 147 insertions(+), 45 deletions(-) create mode 100644 app/common/middleware/InstallGuard.php diff --git a/.gitignore b/.gitignore index 0ebb020..5cd2604 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ yarn.lock /nbproject /runtime/* /install +/public/install node_modules dist dist-ssr diff --git a/app/admin/controller/Ajax.php b/app/admin/controller/Ajax.php index fb6db38..a6a20be 100644 --- a/app/admin/controller/Ajax.php +++ b/app/admin/controller/Ajax.php @@ -17,7 +17,6 @@ use support\Response; class Ajax extends Backend { protected array $noNeedPermission = ['*']; - protected array $noNeedLogin = ['terminal']; public function upload(Request $request): Response { diff --git a/app/admin/controller/Module.php b/app/admin/controller/Module.php index a502037..ce66a26 100644 --- a/app/admin/controller/Module.php +++ b/app/admin/controller/Module.php @@ -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) { diff --git a/app/admin/controller/crud/Crud.php b/app/admin/controller/crud/Crud.php index 2b778b4..cae45e8 100644 --- a/app/admin/controller/crud/Crud.php +++ b/app/admin/controller/crud/Crud.php @@ -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 diff --git a/app/admin/library/module/Manage.php b/app/admin/library/module/Manage.php index d7d21c8..76e6d66 100644 --- a/app/admin/library/module/Manage.php +++ b/app/admin/library/module/Manage.php @@ -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; + } } diff --git a/app/api/controller/Install.php b/app/api/controller/Install.php index 6a9031d..3336c96 100644 --- a/app/api/controller/Install.php +++ b/app/api/controller/Install.php @@ -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!')); } diff --git a/app/common/middleware/InstallGuard.php b/app/common/middleware/InstallGuard.php new file mode 100644 index 0000000..d646728 --- /dev/null +++ b/app/common/middleware/InstallGuard.php @@ -0,0 +1,29 @@ + '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); + } +} diff --git a/app/functions.php b/app/functions.php index e0cf6e5..b20690e 100644 --- a/app/functions.php +++ b/app/functions.php @@ -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 { diff --git a/config/route.php b/config/route.php index beb5002..d66910f 100644 --- a/config/route.php +++ b/config/route.php @@ -10,12 +10,9 @@ use support\Response; // ==================== 未安装时根路径重定向(迁移自 public/index.php) ==================== // 当 install.lock 不存在或未完成安装时,访问 / 或 /index.html 重定向到安装页 -$installLockFile = public_path('install.lock'); -$installCompleteMark = 'install-end'; $installPageFile = public_path('install/index.html'); -Route::get('/', function () use ($installLockFile, $installCompleteMark, $installPageFile) { - $needRedirect = is_file($installPageFile) - && (!is_file($installLockFile) || @file_get_contents($installLockFile) !== $installCompleteMark); +Route::get('/', function () use ($installPageFile) { + $needRedirect = is_file($installPageFile) && !is_system_installed(); if ($needRedirect) { return new Response(302, ['Location' => '/install/']); } @@ -24,9 +21,8 @@ Route::get('/', function () use ($installLockFile, $installCompleteMark, $instal } return new Response(404, [], 'Not Found'); }); -Route::get('/index.html', function () use ($installLockFile, $installCompleteMark, $installPageFile) { - $needRedirect = is_file($installPageFile) - && (!is_file($installLockFile) || @file_get_contents($installLockFile) !== $installCompleteMark); +Route::get('/index.html', function () use ($installPageFile) { + $needRedirect = is_file($installPageFile) && !is_system_installed(); if ($needRedirect) { return new Response(302, ['Location' => '/install/']); } @@ -36,26 +32,24 @@ Route::get('/index.html', function () use ($installLockFile, $installCompleteMar // ==================== 安装向导(静态页) ==================== // 已安装时访问 /install 重定向到应用,访问提示仅在终端显示 -$installLockFileForInstall = public_path('install.lock'); -$installCompleteMarkForInstall = 'install-end'; -Route::get('/install', function () use ($installLockFileForInstall, $installCompleteMarkForInstall) { - $installed = is_file($installLockFileForInstall) && @file_get_contents($installLockFileForInstall) === $installCompleteMarkForInstall; +Route::get('/install', function () { + $installed = is_system_installed(); if ($installed && is_file(public_path('index.html'))) { return new Response(302, ['Location' => '/index.html']); } $file = public_path('install/index.html'); return is_file($file) ? (new Response())->file($file) : new Response(404, [], 'Install page not found'); }); -Route::get('/install/', function () use ($installLockFileForInstall, $installCompleteMarkForInstall) { - $installed = is_file($installLockFileForInstall) && @file_get_contents($installLockFileForInstall) === $installCompleteMarkForInstall; +Route::get('/install/', function () { + $installed = is_system_installed(); if ($installed && is_file(public_path('index.html'))) { return new Response(302, ['Location' => '/index.html']); } $file = public_path('install/index.html'); return is_file($file) ? (new Response())->file($file) : new Response(404, [], 'Install page not found'); }); -Route::get('/install/index', function () use ($installLockFileForInstall, $installCompleteMarkForInstall) { - $installed = is_file($installLockFileForInstall) && @file_get_contents($installLockFileForInstall) === $installCompleteMarkForInstall; +Route::get('/install/index', function () { + $installed = is_system_installed(); if ($installed && is_file(public_path('index.html'))) { return new Response(302, ['Location' => '/index.html']); } @@ -72,17 +66,19 @@ Route::get('/api/index/index', [\app\api\controller\Index::class, 'index']); Route::add(['GET', 'POST'], '/api/user/checkIn', [\app\api\controller\User::class, 'checkIn']); Route::post('/api/user/logout', [\app\api\controller\User::class, 'logout']); -// api/install(安装流程多为 POST) -Route::add(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD'], '/api/install/terminal', [\app\api\controller\Install::class, 'terminal']); -Route::post('/api/install/changePackageManager', [\app\api\controller\Install::class, 'changePackageManager']); -Route::get('/api/install/envBaseCheck', [\app\api\controller\Install::class, 'envBaseCheck']); -Route::add(['GET', 'POST'], '/api/install/envNpmCheck', [\app\api\controller\Install::class, 'envNpmCheck']); -Route::post('/api/install/testDatabase', [\app\api\controller\Install::class, 'testDatabase']); -Route::add(['GET', 'POST'], '/api/install/baseConfig', [\app\api\controller\Install::class, 'baseConfig']); -Route::get('/api/install/accessUrls', [\app\api\controller\Install::class, 'accessUrls']); -Route::post('/api/install/commandExecComplete', [\app\api\controller\Install::class, 'commandExecComplete']); -Route::post('/api/install/manualInstall', [\app\api\controller\Install::class, 'manualInstall']); -Route::post('/api/install/mvDist', [\app\api\controller\Install::class, 'mvDist']); +// api/install(安装流程多为 POST;已安装系统由 InstallGuard 拦截) +Route::group('/api/install', function () { + Route::add(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD'], '/terminal', [\app\api\controller\Install::class, 'terminal']); + Route::post('/changePackageManager', [\app\api\controller\Install::class, 'changePackageManager']); + Route::get('/envBaseCheck', [\app\api\controller\Install::class, 'envBaseCheck']); + Route::add(['GET', 'POST'], '/envNpmCheck', [\app\api\controller\Install::class, 'envNpmCheck']); + Route::post('/testDatabase', [\app\api\controller\Install::class, 'testDatabase']); + Route::add(['GET', 'POST'], '/baseConfig', [\app\api\controller\Install::class, 'baseConfig']); + Route::get('/accessUrls', [\app\api\controller\Install::class, 'accessUrls']); + Route::post('/commandExecComplete', [\app\api\controller\Install::class, 'commandExecComplete']); + Route::post('/manualInstall', [\app\api\controller\Install::class, 'manualInstall']); + Route::post('/mvDist', [\app\api\controller\Install::class, 'mvDist']); +})->middleware([\app\common\middleware\InstallGuard::class]); // api/common Route::get('/api/common/captcha', [\app\api\controller\Common::class, 'captcha']);