Compare commits

..

2 Commits

Author SHA1 Message Date
Copilot 6bec52dca1 Fix /open/user/init auth bypass allowing credential reset on initialized systems (#2941)
* Initial plan

* fix: add /open/user/init paths to init guard to prevent auth bypass

Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>
Co-authored-by: whyour <imwhyour@gmail.com>
2026-03-01 18:02:21 +08:00
rockymelody ce599d306f 青龙面板鉴权绕过漏洞已修复 (#2935)
已实施的安全加固措施
第一层防御:启用Express严格路由(第17-18行)
app.set('case sensitive routing', true);  // 路由大小写敏感
app.set('strict routing', true);           // 严格路由匹配
第二层防御:路径标准化检查中间件(第23-37行)
app.use((req, res, next) => {
  const originalPath = req.path;
  const normalizedPath = originalPath.toLowerCase();

  // 检测并拦截大小写混淆攻击
  if (originalPath !== normalizedPath &&
      (normalizedPath.startsWith('/api/') || normalizedPath.startsWith('/open/'))) {
    return res.status(400).json({
      code: 400,
      message: 'Invalid path format'
    });
  }

  next();
});
作用:主动检测并拒绝含有大小写变体的恶意请求
第三层防御:JWT中间件正则表达式修复(第59行)
// 修复前:
path: [...config.apiWhiteList, /^\/(?!api\/).*/],

// 修复后:添加大小写不敏感标志 'i'
path: [...config.apiWhiteList, /^(\/(?!api\/).*)$/i],
作用:防御正则匹配层面的绕过
第四层防御:自定义Token中间件路径标准化(第74-87行)
// 修复前:
if (!['/open/', '/api/'].some((x) => req.path.startsWith(x))) {

// 修复后:统一转小写比较
const pathLower = req.path.toLowerCase();
if (!['/open/', '/api/'].some((x) => pathLower.startsWith(x))) {
}
作用:确保Token验证逻辑对所有路径变体生效

第五层防御:初始化接口路径检查修复(第122-123行)
// 修复前:
if (!['/api/user/init', '/api/user/notification/init'].includes(req.path)) {

// 修复后:
const pathLower = req.path.toLowerCase();
if (!['/api/user/init', '/api/user/notification/init'].includes(pathLower)) {
2026-03-01 17:44:03 +08:00
3 changed files with 58 additions and 175 deletions
-160
View File
@@ -10,7 +10,6 @@ import Logger from '../loaders/logger';
import { writeFileWithLock } from '../shared/utils';
import { DependenceTypes } from '../data/dependence';
import { FormData } from 'undici';
import os from 'os';
export * from './share';
@@ -591,162 +590,3 @@ export function getUninstallCommand(
export function isDemoEnv() {
return process.env.DeployEnv === 'demo';
}
// OS detection for Linux mirror configuration
let osType: 'Debian' | 'Ubuntu' | 'Alpine' | undefined;
async function getOSReleaseInfo(): Promise<string> {
try {
const osRelease = await fs.readFile('/etc/os-release', 'utf8');
return osRelease;
} catch (error) {
Logger.error(`Failed to read /etc/os-release: ${error}`);
return '';
}
}
function isDebian(osReleaseInfo: string): boolean {
return osReleaseInfo.includes('Debian');
}
function isUbuntu(osReleaseInfo: string): boolean {
return osReleaseInfo.includes('Ubuntu');
}
function isAlpine(osReleaseInfo: string): boolean {
return osReleaseInfo.includes('Alpine');
}
export async function detectOS(): Promise<
'Debian' | 'Ubuntu' | 'Alpine' | undefined
> {
if (osType) return osType;
const platform = os.platform();
if (platform === 'linux') {
const osReleaseInfo = await getOSReleaseInfo();
// Check Ubuntu before Debian since Ubuntu is based on Debian
if (isUbuntu(osReleaseInfo)) {
osType = 'Ubuntu';
} else if (isDebian(osReleaseInfo)) {
osType = 'Debian';
} else if (isAlpine(osReleaseInfo)) {
osType = 'Alpine';
} else {
Logger.error(`Unknown Linux Distribution: ${osReleaseInfo}`);
console.error(`Unknown Linux Distribution: ${osReleaseInfo}`);
}
} else if (platform === 'darwin') {
osType = undefined;
} else {
Logger.error(`Unsupported platform: ${platform}`);
console.error(`Unsupported platform: ${platform}`);
}
return osType;
}
async function getCurrentMirrorDomain(
filePath: string,
): Promise<string | null> {
try {
const fileContent = await fs.readFile(filePath, 'utf8');
const lines = fileContent.split('\n');
for (const line of lines) {
if (line.trim().startsWith('#')) {
continue;
}
const match = line.match(/https?:\/\/[^\/]+/);
if (match) {
return match[0];
}
}
return null;
} catch (error) {
Logger.error(`Failed to read mirror configuration file ${filePath}: ${error}`);
return null;
}
}
function escapeRegExp(string: string): string {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
async function replaceDomainInFile(
filePath: string,
oldDomainWithScheme: string,
newDomainWithScheme: string,
): Promise<void> {
// Ensure the new domain has a trailing slash before replacement
if (!newDomainWithScheme.endsWith('/')) {
newDomainWithScheme += '/';
}
let fileContent = await fs.readFile(filePath, 'utf8');
// Escape special regex characters in the old domain
const escapedOldDomain = escapeRegExp(oldDomainWithScheme);
let updatedContent = fileContent.replace(
new RegExp(escapedOldDomain, 'g'),
newDomainWithScheme,
);
await writeFileWithLock(filePath, updatedContent);
}
async function _updateLinuxMirror(
osType: string,
mirrorDomainWithScheme: string,
): Promise<string> {
let filePath: string, currentDomainWithScheme: string | null;
switch (osType) {
case 'Debian':
filePath = '/etc/apt/sources.list.d/debian.sources';
currentDomainWithScheme = await getCurrentMirrorDomain(filePath);
if (currentDomainWithScheme) {
await replaceDomainInFile(
filePath,
currentDomainWithScheme,
mirrorDomainWithScheme || 'http://deb.debian.org',
);
return 'apt-get update';
} else {
throw Error(`Current mirror domain not found.`);
}
case 'Ubuntu':
filePath = '/etc/apt/sources.list.d/ubuntu.sources';
currentDomainWithScheme = await getCurrentMirrorDomain(filePath);
if (currentDomainWithScheme) {
await replaceDomainInFile(
filePath,
currentDomainWithScheme,
mirrorDomainWithScheme || 'http://archive.ubuntu.com',
);
return 'apt-get update';
} else {
throw Error(`Current mirror domain not found.`);
}
case 'Alpine':
filePath = '/etc/apk/repositories';
currentDomainWithScheme = await getCurrentMirrorDomain(filePath);
if (currentDomainWithScheme) {
await replaceDomainInFile(
filePath,
currentDomainWithScheme,
mirrorDomainWithScheme || 'http://dl-cdn.alpinelinux.org',
);
return 'apk update';
} else {
throw Error(`Current mirror domain not found.`);
}
default:
throw Error('Unsupported OS type for updating mirrors.');
}
}
export async function updateLinuxMirrorFile(mirror: string): Promise<string> {
const detectedOS = await detectOS();
if (!detectedOS) {
throw Error(`Unknown Linux Distribution`);
}
return await _updateLinuxMirror(detectedOS, mirror);
}
+34 -5
View File
@@ -13,9 +13,29 @@ import { isValidToken } from '../shared/auth';
import path from 'path';
export default ({ app }: { app: Application }) => {
// Security: Enable strict routing to prevent case-insensitive path bypass
app.set('case sensitive routing', true);
app.set('strict routing', true);
app.set('trust proxy', 'loopback');
app.use(cors());
// Security: Path normalization middleware to prevent case variation attacks
app.use((req, res, next) => {
const originalPath = req.path;
const normalizedPath = originalPath.toLowerCase();
// Block requests with case variations on protected paths
if (originalPath !== normalizedPath &&
(normalizedPath.startsWith('/api/') || normalizedPath.startsWith('/open/'))) {
return res.status(400).json({
code: 400,
message: 'Invalid path format'
});
}
next();
});
// Rewrite URLs to strip baseUrl prefix if configured
// This allows the rest of the app to work without baseUrl awareness
if (config.baseUrl) {
@@ -36,7 +56,7 @@ export default ({ app }: { app: Application }) => {
secret: config.jwt.secret,
algorithms: ['HS384'],
}).unless({
path: [...config.apiWhiteList, /^\/(?!api\/).*/],
path: [...config.apiWhiteList, /^(\/(?!api\/).*)$/i],
}),
);
@@ -51,19 +71,20 @@ export default ({ app }: { app: Application }) => {
});
app.use(async (req: Request, res, next) => {
if (!['/open/', '/api/'].some((x) => req.path.startsWith(x))) {
const pathLower = req.path.toLowerCase();
if (!['/open/', '/api/'].some((x) => pathLower.startsWith(x))) {
return next();
}
const headerToken = getToken(req);
if (req.path.startsWith('/open/')) {
if (pathLower.startsWith('/open/')) {
const apps = await shareStore.getApps();
const doc = apps?.filter((x) =>
x.tokens?.find((y) => y.value === headerToken),
)?.[0];
if (doc && doc.tokens && doc.tokens.length > 0) {
const currentToken = doc.tokens.find((x) => x.value === headerToken);
const keyMatch = req.path.match(/\/open\/([a-z]+)\/*/);
const keyMatch = pathLower.match(/\/open\/([a-z]+)\/*/);
const key = keyMatch && keyMatch[1];
if (
doc.scopes.includes(key as any) &&
@@ -98,7 +119,15 @@ export default ({ app }: { app: Application }) => {
});
app.use(async (req, res, next) => {
if (!['/api/user/init', '/api/user/notification/init'].includes(req.path)) {
const pathLower = req.path.toLowerCase();
if (
![
'/api/user/init',
'/api/user/notification/init',
'/open/user/init',
'/open/user/notification/init',
].includes(req.path)
) {
return next();
}
const authInfo =
+24 -10
View File
@@ -17,7 +17,6 @@ import {
readDirs,
rmPath,
setSystemTimezone,
updateLinuxMirrorFile,
} from '../config/util';
import {
DependenceModel,
@@ -215,11 +214,33 @@ export default class SystemService {
onEnd?: () => void,
) {
const oDoc = await this.getSystemConfig();
await this.updateAuthDb({
...oDoc,
info: { ...oDoc.info, ...info },
});
let defaultDomain = 'https://dl-cdn.alpinelinux.org';
let targetDomain = 'https://dl-cdn.alpinelinux.org';
if (os.platform() !== 'linux') {
return;
}
const command = await updateLinuxMirrorFile(info.linuxMirror || '');
let hasError = false;
const content = await fs.promises.readFile('/etc/apk/repositories', {
encoding: 'utf-8',
});
const domainMatch = content.match(/(http.*)\/alpine\/.*/);
if (domainMatch) {
defaultDomain = domainMatch[1];
}
if (info.linuxMirror) {
targetDomain = info.linuxMirror;
}
const command = `sed -i 's/${defaultDomain.replace(
/\//g,
'\\/',
)}/${targetDomain.replace(
/\//g,
'\\/',
)}/g' /etc/apk/repositories && apk update -f`;
this.scheduleService.runTask(
command,
{
@@ -233,15 +254,8 @@ export default class SystemService {
message: 'update linux mirror end',
});
onEnd?.();
if (!hasError) {
await this.updateAuthDb({
...oDoc,
info: { ...oDoc.info, ...info },
});
}
},
onError: async (message: string) => {
hasError = true;
this.sockService.sendMessage({ type: 'updateLinuxMirror', message });
},
onLog: async (message: string) => {