1.修复上传漏洞和SQL注入漏洞
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -16,6 +16,7 @@ yarn.lock
|
|||||||
/nbproject
|
/nbproject
|
||||||
/runtime/*
|
/runtime/*
|
||||||
/install
|
/install
|
||||||
|
/public/install
|
||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
dist-ssr
|
dist-ssr
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ use support\Response;
|
|||||||
class Ajax extends Backend
|
class Ajax extends Backend
|
||||||
{
|
{
|
||||||
protected array $noNeedPermission = ['*'];
|
protected array $noNeedPermission = ['*'];
|
||||||
protected array $noNeedLogin = ['terminal'];
|
|
||||||
|
|
||||||
public function upload(Request $request): Response
|
public function upload(Request $request): Response
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -131,12 +131,23 @@ class Module extends Backend
|
|||||||
if ($response !== null) return $response;
|
if ($response !== null) return $response;
|
||||||
|
|
||||||
AdminLog::instance($request)->setTitle(__('Upload module'));
|
AdminLog::instance($request)->setTitle(__('Upload module'));
|
||||||
$file = $request->file('file');
|
|
||||||
if (!$file) {
|
$token = $request->post('token', $request->get('token', ''));
|
||||||
return $this->error(__('Parameter error'));
|
if (!$token) {
|
||||||
|
return $this->error(__('Please login to the official website account first'));
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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) {
|
} catch (BaException $e) {
|
||||||
return $this->error(__($e->getMessage()), $e->getData(), $e->getCode());
|
return $this->error(__($e->getMessage()), $e->getData(), $e->getCode());
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ class Crud extends Backend
|
|||||||
protected string $webTranslate = '';
|
protected string $webTranslate = '';
|
||||||
protected array $langTsData = [];
|
protected array $langTsData = [];
|
||||||
protected array $dtStringToArray = ['checkbox', 'selects', 'remoteSelects', 'city', 'images', 'files'];
|
protected array $dtStringToArray = ['checkbox', 'selects', 'remoteSelects', 'city', 'images', 'files'];
|
||||||
protected array $noNeedLogin = ['getFileData'];
|
|
||||||
protected array $noNeedPermission = ['logStart', 'getFileData', 'parseFieldData', 'generateCheck', 'uploadCompleted'];
|
protected array $noNeedPermission = ['logStart', 'getFileData', 'parseFieldData', 'generateCheck', 'uploadCompleted'];
|
||||||
|
|
||||||
protected function initController(Request $request): ?Response
|
protected function initController(Request $request): ?Response
|
||||||
|
|||||||
@@ -95,7 +95,12 @@ class Manage
|
|||||||
if (!is_dir($uploadDir)) {
|
if (!is_dir($uploadDir)) {
|
||||||
mkdir($uploadDir, 0755, true);
|
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;
|
$savePath = $uploadDir . $saveName;
|
||||||
$saveDir = dirname($savePath);
|
$saveDir = dirname($savePath);
|
||||||
if (!is_dir($saveDir)) {
|
if (!is_dir($saveDir)) {
|
||||||
@@ -155,10 +160,7 @@ class Manage
|
|||||||
*/
|
*/
|
||||||
public function doUpload(string $token, string $file): array
|
public function doUpload(string $token, string $file): array
|
||||||
{
|
{
|
||||||
$file = Filesystem::fsFit(root_path() . 'public' . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $file));
|
$file = self::resolvePublicStorageFile($file);
|
||||||
if (!is_file($file)) {
|
|
||||||
throw new Exception('Zip file not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
$copyTo = $this->installDir . 'uploadTemp' . date('YmdHis') . '.zip';
|
$copyTo = $this->installDir . 'uploadTemp' . date('YmdHis') . '.zip';
|
||||||
copy($file, $copyTo);
|
copy($file, $copyTo);
|
||||||
@@ -897,4 +899,52 @@ class Manage
|
|||||||
$this->modulesDir = $this->installDir . $uid . DIRECTORY_SEPARATOR;
|
$this->modulesDir = $this->installDir . $uid . DIRECTORY_SEPARATOR;
|
||||||
return $this;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -389,6 +389,9 @@ class Install extends Api
|
|||||||
public function testDatabase(Request $request): Response
|
public function testDatabase(Request $request): Response
|
||||||
{
|
{
|
||||||
$this->setRequest($request);
|
$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 = [
|
$database = [
|
||||||
'hostname' => $request->post('hostname'),
|
'hostname' => $request->post('hostname'),
|
||||||
'username' => $request->post('username'),
|
'username' => $request->post('username'),
|
||||||
@@ -571,13 +574,7 @@ class Install extends Api
|
|||||||
|
|
||||||
protected function isInstallComplete(): bool
|
protected function isInstallComplete(): bool
|
||||||
{
|
{
|
||||||
if (is_file(public_path(self::$lockFileName))) {
|
return is_system_installed();
|
||||||
$contents = @file_get_contents(public_path(self::$lockFileName));
|
|
||||||
if ($contents == self::$InstallationCompletionMark) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -656,6 +653,9 @@ class Install extends Api
|
|||||||
public function accessUrls(Request $request): Response
|
public function accessUrls(Request $request): Response
|
||||||
{
|
{
|
||||||
$this->setRequest($request);
|
$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');
|
$host = $request->header('host', '127.0.0.1:8787');
|
||||||
$port = '8787';
|
$port = '8787';
|
||||||
if (str_contains($host, ':')) {
|
if (str_contains($host, ':')) {
|
||||||
@@ -684,6 +684,9 @@ class Install extends Api
|
|||||||
public function manualInstall(Request $request): Response
|
public function manualInstall(Request $request): Response
|
||||||
{
|
{
|
||||||
$this->setRequest($request);
|
$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('', [
|
return $this->success('', [
|
||||||
'webPath' => str_replace('\\', '/', root_path() . 'web')
|
'webPath' => str_replace('\\', '/', root_path() . 'web')
|
||||||
]);
|
]);
|
||||||
@@ -692,6 +695,9 @@ class Install extends Api
|
|||||||
public function mvDist(Request $request): Response
|
public function mvDist(Request $request): Response
|
||||||
{
|
{
|
||||||
$this->setRequest($request);
|
$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')) {
|
if (!is_file(root_path() . self::$distDir . DIRECTORY_SEPARATOR . 'index.html')) {
|
||||||
return $this->error(__('No built front-end file found, please rebuild manually!'));
|
return $this->error(__('No built front-end file found, please rebuild manually!'));
|
||||||
}
|
}
|
||||||
|
|||||||
29
app/common/middleware/InstallGuard.php
Normal file
29
app/common/middleware/InstallGuard.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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')) {
|
if (!function_exists('get_area')) {
|
||||||
function get_area($request = null): array
|
function get_area($request = null): array
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,12 +10,9 @@ use support\Response;
|
|||||||
|
|
||||||
// ==================== 未安装时根路径重定向(迁移自 public/index.php) ====================
|
// ==================== 未安装时根路径重定向(迁移自 public/index.php) ====================
|
||||||
// 当 install.lock 不存在或未完成安装时,访问 / 或 /index.html 重定向到安装页
|
// 当 install.lock 不存在或未完成安装时,访问 / 或 /index.html 重定向到安装页
|
||||||
$installLockFile = public_path('install.lock');
|
|
||||||
$installCompleteMark = 'install-end';
|
|
||||||
$installPageFile = public_path('install/index.html');
|
$installPageFile = public_path('install/index.html');
|
||||||
Route::get('/', function () use ($installLockFile, $installCompleteMark, $installPageFile) {
|
Route::get('/', function () use ($installPageFile) {
|
||||||
$needRedirect = is_file($installPageFile)
|
$needRedirect = is_file($installPageFile) && !is_system_installed();
|
||||||
&& (!is_file($installLockFile) || @file_get_contents($installLockFile) !== $installCompleteMark);
|
|
||||||
if ($needRedirect) {
|
if ($needRedirect) {
|
||||||
return new Response(302, ['Location' => '/install/']);
|
return new Response(302, ['Location' => '/install/']);
|
||||||
}
|
}
|
||||||
@@ -24,9 +21,8 @@ Route::get('/', function () use ($installLockFile, $installCompleteMark, $instal
|
|||||||
}
|
}
|
||||||
return new Response(404, [], 'Not Found');
|
return new Response(404, [], 'Not Found');
|
||||||
});
|
});
|
||||||
Route::get('/index.html', function () use ($installLockFile, $installCompleteMark, $installPageFile) {
|
Route::get('/index.html', function () use ($installPageFile) {
|
||||||
$needRedirect = is_file($installPageFile)
|
$needRedirect = is_file($installPageFile) && !is_system_installed();
|
||||||
&& (!is_file($installLockFile) || @file_get_contents($installLockFile) !== $installCompleteMark);
|
|
||||||
if ($needRedirect) {
|
if ($needRedirect) {
|
||||||
return new Response(302, ['Location' => '/install/']);
|
return new Response(302, ['Location' => '/install/']);
|
||||||
}
|
}
|
||||||
@@ -36,26 +32,24 @@ Route::get('/index.html', function () use ($installLockFile, $installCompleteMar
|
|||||||
|
|
||||||
// ==================== 安装向导(静态页) ====================
|
// ==================== 安装向导(静态页) ====================
|
||||||
// 已安装时访问 /install 重定向到应用,访问提示仅在终端显示
|
// 已安装时访问 /install 重定向到应用,访问提示仅在终端显示
|
||||||
$installLockFileForInstall = public_path('install.lock');
|
Route::get('/install', function () {
|
||||||
$installCompleteMarkForInstall = 'install-end';
|
$installed = is_system_installed();
|
||||||
Route::get('/install', function () use ($installLockFileForInstall, $installCompleteMarkForInstall) {
|
|
||||||
$installed = is_file($installLockFileForInstall) && @file_get_contents($installLockFileForInstall) === $installCompleteMarkForInstall;
|
|
||||||
if ($installed && is_file(public_path('index.html'))) {
|
if ($installed && is_file(public_path('index.html'))) {
|
||||||
return new Response(302, ['Location' => '/index.html']);
|
return new Response(302, ['Location' => '/index.html']);
|
||||||
}
|
}
|
||||||
$file = public_path('install/index.html');
|
$file = public_path('install/index.html');
|
||||||
return is_file($file) ? (new Response())->file($file) : new Response(404, [], 'Install page not found');
|
return is_file($file) ? (new Response())->file($file) : new Response(404, [], 'Install page not found');
|
||||||
});
|
});
|
||||||
Route::get('/install/', function () use ($installLockFileForInstall, $installCompleteMarkForInstall) {
|
Route::get('/install/', function () {
|
||||||
$installed = is_file($installLockFileForInstall) && @file_get_contents($installLockFileForInstall) === $installCompleteMarkForInstall;
|
$installed = is_system_installed();
|
||||||
if ($installed && is_file(public_path('index.html'))) {
|
if ($installed && is_file(public_path('index.html'))) {
|
||||||
return new Response(302, ['Location' => '/index.html']);
|
return new Response(302, ['Location' => '/index.html']);
|
||||||
}
|
}
|
||||||
$file = public_path('install/index.html');
|
$file = public_path('install/index.html');
|
||||||
return is_file($file) ? (new Response())->file($file) : new Response(404, [], 'Install page not found');
|
return is_file($file) ? (new Response())->file($file) : new Response(404, [], 'Install page not found');
|
||||||
});
|
});
|
||||||
Route::get('/install/index', function () use ($installLockFileForInstall, $installCompleteMarkForInstall) {
|
Route::get('/install/index', function () {
|
||||||
$installed = is_file($installLockFileForInstall) && @file_get_contents($installLockFileForInstall) === $installCompleteMarkForInstall;
|
$installed = is_system_installed();
|
||||||
if ($installed && is_file(public_path('index.html'))) {
|
if ($installed && is_file(public_path('index.html'))) {
|
||||||
return new Response(302, ['Location' => '/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::add(['GET', 'POST'], '/api/user/checkIn', [\app\api\controller\User::class, 'checkIn']);
|
||||||
Route::post('/api/user/logout', [\app\api\controller\User::class, 'logout']);
|
Route::post('/api/user/logout', [\app\api\controller\User::class, 'logout']);
|
||||||
|
|
||||||
// api/install(安装流程多为 POST)
|
// api/install(安装流程多为 POST;已安装系统由 InstallGuard 拦截)
|
||||||
Route::add(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD'], '/api/install/terminal', [\app\api\controller\Install::class, 'terminal']);
|
Route::group('/api/install', function () {
|
||||||
Route::post('/api/install/changePackageManager', [\app\api\controller\Install::class, 'changePackageManager']);
|
Route::add(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD'], '/terminal', [\app\api\controller\Install::class, 'terminal']);
|
||||||
Route::get('/api/install/envBaseCheck', [\app\api\controller\Install::class, 'envBaseCheck']);
|
Route::post('/changePackageManager', [\app\api\controller\Install::class, 'changePackageManager']);
|
||||||
Route::add(['GET', 'POST'], '/api/install/envNpmCheck', [\app\api\controller\Install::class, 'envNpmCheck']);
|
Route::get('/envBaseCheck', [\app\api\controller\Install::class, 'envBaseCheck']);
|
||||||
Route::post('/api/install/testDatabase', [\app\api\controller\Install::class, 'testDatabase']);
|
Route::add(['GET', 'POST'], '/envNpmCheck', [\app\api\controller\Install::class, 'envNpmCheck']);
|
||||||
Route::add(['GET', 'POST'], '/api/install/baseConfig', [\app\api\controller\Install::class, 'baseConfig']);
|
Route::post('/testDatabase', [\app\api\controller\Install::class, 'testDatabase']);
|
||||||
Route::get('/api/install/accessUrls', [\app\api\controller\Install::class, 'accessUrls']);
|
Route::add(['GET', 'POST'], '/baseConfig', [\app\api\controller\Install::class, 'baseConfig']);
|
||||||
Route::post('/api/install/commandExecComplete', [\app\api\controller\Install::class, 'commandExecComplete']);
|
Route::get('/accessUrls', [\app\api\controller\Install::class, 'accessUrls']);
|
||||||
Route::post('/api/install/manualInstall', [\app\api\controller\Install::class, 'manualInstall']);
|
Route::post('/commandExecComplete', [\app\api\controller\Install::class, 'commandExecComplete']);
|
||||||
Route::post('/api/install/mvDist', [\app\api\controller\Install::class, 'mvDist']);
|
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
|
// api/common
|
||||||
Route::get('/api/common/captcha', [\app\api\controller\Common::class, 'captcha']);
|
Route::get('/api/common/captcha', [\app\api\controller\Common::class, 'captcha']);
|
||||||
|
|||||||
Reference in New Issue
Block a user