mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2025-12-18 20:30:08 +08:00
feat: 增强文件路径处理逻辑,添加安全验证以防止路径遍历攻击,并优化查询参数提取
This commit is contained in:
parent
2f9bcd88ba
commit
84fa9da762
@ -92,42 +92,65 @@ export default function WebLoginPage() {
|
||||
</CardHeader>
|
||||
|
||||
<CardBody className="flex gap-5 py-5 px-5 md:px-10">
|
||||
<Input
|
||||
isClearable
|
||||
type="password"
|
||||
classNames={{
|
||||
label: 'text-black/50 dark:text-white/90',
|
||||
input: [
|
||||
'bg-transparent',
|
||||
'text-black/90 dark:text-white/90',
|
||||
'placeholder:text-default-700/50 dark:placeholder:text-white/60'
|
||||
],
|
||||
innerWrapper: 'bg-transparent',
|
||||
inputWrapper: [
|
||||
'shadow-xl',
|
||||
'bg-default-100/70',
|
||||
'dark:bg-default/60',
|
||||
'backdrop-blur-xl',
|
||||
'backdrop-saturate-200',
|
||||
'hover:bg-default-0/70',
|
||||
'dark:hover:bg-default/70',
|
||||
'group-data-[focus=true]:bg-default-100/50',
|
||||
'dark:group-data-[focus=true]:bg-default/60',
|
||||
'!cursor-text'
|
||||
]
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
onSubmit()
|
||||
}}
|
||||
isDisabled={isLoading}
|
||||
label="Token"
|
||||
placeholder="请输入token"
|
||||
radius="lg"
|
||||
size="lg"
|
||||
startContent={
|
||||
<IoKeyOutline className="text-black/50 mb-0.5 dark:text-white/90 text-slate-400 pointer-events-none flex-shrink-0" />
|
||||
}
|
||||
value={tokenValue}
|
||||
onChange={(e) => setTokenValue(e.target.value)}
|
||||
onClear={() => setTokenValue('')}
|
||||
/>
|
||||
>
|
||||
{/* 隐藏的用户名字段,帮助浏览器识别登录表单 */}
|
||||
<input
|
||||
type="text"
|
||||
name="username"
|
||||
value="napcat-webui"
|
||||
autoComplete="username"
|
||||
className="absolute -left-[9999px] opacity-0 pointer-events-none"
|
||||
readOnly
|
||||
tabIndex={-1}
|
||||
aria-label="Username"
|
||||
/>
|
||||
<Input
|
||||
isClearable
|
||||
type="password"
|
||||
name="password"
|
||||
autoComplete="current-password"
|
||||
classNames={{
|
||||
label: 'text-black/50 dark:text-white/90',
|
||||
input: [
|
||||
'bg-transparent',
|
||||
'text-black/90 dark:text-white/90',
|
||||
'placeholder:text-default-700/50 dark:placeholder:text-white/60'
|
||||
],
|
||||
innerWrapper: 'bg-transparent',
|
||||
inputWrapper: [
|
||||
'shadow-xl',
|
||||
'bg-default-100/70',
|
||||
'dark:bg-default/60',
|
||||
'backdrop-blur-xl',
|
||||
'backdrop-saturate-200',
|
||||
'hover:bg-default-0/70',
|
||||
'dark:hover:bg-default/70',
|
||||
'group-data-[focus=true]:bg-default-100/50',
|
||||
'dark:group-data-[focus=true]:bg-default/60',
|
||||
'!cursor-text'
|
||||
]
|
||||
}}
|
||||
isDisabled={isLoading}
|
||||
label="Token"
|
||||
placeholder="请输入token"
|
||||
radius="lg"
|
||||
size="lg"
|
||||
startContent={
|
||||
<IoKeyOutline className="text-black/50 mb-0.5 dark:text-white/90 text-slate-400 pointer-events-none flex-shrink-0" />
|
||||
}
|
||||
value={tokenValue}
|
||||
onChange={(e) => setTokenValue(e.target.value)}
|
||||
onClear={() => setTokenValue('')}
|
||||
/>
|
||||
</form>
|
||||
<div className="text-center text-small text-default-600 dark:text-default-400 px-2">
|
||||
💡 提示:请从 NapCat 启动日志中查看登录密钥
|
||||
</div>
|
||||
<Button
|
||||
className="mx-10 mt-10 text-lg py-7"
|
||||
color="primary"
|
||||
|
||||
@ -216,6 +216,7 @@ export async function InitWebUi(logger: LogWrapper, pathWrapper: NapCatPathWrapp
|
||||
// ------------启动服务------------
|
||||
server.listen(port, host, async () => {
|
||||
let searchParams = { token: token };
|
||||
logger.log(`[NapCat] [WebUi] 🔑 token=${token}`);
|
||||
logger.log(
|
||||
`[NapCat] [WebUi] WebUi User Panel Url: ${createUrl('127.0.0.1', port.toString(), '/webui', searchParams)}`
|
||||
);
|
||||
|
||||
@ -13,6 +13,14 @@ import { WebUiConfig, getInitialWebUiToken, webUiPathWrapper } from '@/webui';
|
||||
|
||||
const isWindows = os.platform() === 'win32';
|
||||
|
||||
// 安全地从查询参数中提取字符串值,防止类型混淆
|
||||
const getQueryStringParam = (param: any): string => {
|
||||
if (Array.isArray(param)) {
|
||||
return String(param[0] || '');
|
||||
}
|
||||
return String(param || '');
|
||||
};
|
||||
|
||||
// 获取系统根目录列表(Windows返回盘符列表,其他系统返回['/'])
|
||||
const getRootDirs = async (): Promise<string[]> => {
|
||||
if (!isWindows) return ['/'];
|
||||
@ -34,21 +42,29 @@ const getRootDirs = async (): Promise<string[]> => {
|
||||
|
||||
// 规范化路径并进行安全验证
|
||||
const normalizePath = (inputPath: string): string => {
|
||||
if (!inputPath) return isWindows ? 'C:\\' : '/';
|
||||
if (!inputPath) {
|
||||
// 对于空路径,Windows返回用户主目录,其他系统返回根目录
|
||||
return isWindows ? process.env['USERPROFILE'] || 'C:\\' : '/';
|
||||
}
|
||||
|
||||
// 如果是Windows且输入为纯盘符(可能带或不带斜杠),统一返回 "X:\"
|
||||
if (isWindows && /^[A-Z]:[\\/]*$/i.test(inputPath)) {
|
||||
return inputPath.slice(0, 2) + '\\';
|
||||
}
|
||||
|
||||
// 进行路径规范化
|
||||
const normalized = path.normalize(inputPath);
|
||||
|
||||
// 安全验证:检查是否包含危险的路径遍历模式
|
||||
if (containsPathTraversal(normalized)) {
|
||||
// 安全验证:检查是否包含危险的路径遍历模式(在规范化之前)
|
||||
if (containsPathTraversal(inputPath)) {
|
||||
throw new Error('Invalid path: path traversal detected');
|
||||
}
|
||||
|
||||
// 进行路径规范化
|
||||
const normalized = path.resolve(inputPath);
|
||||
|
||||
// 再次检查规范化后的路径,确保没有绕过安全检查
|
||||
if (containsPathTraversal(normalized)) {
|
||||
throw new Error('Invalid path: path traversal detected after normalization');
|
||||
}
|
||||
|
||||
return normalized;
|
||||
};
|
||||
|
||||
@ -125,7 +141,7 @@ export const ListFilesHandler: RequestHandler = async (req, res) => {
|
||||
return sendError(res, '默认密码禁止使用');
|
||||
}
|
||||
try {
|
||||
const requestPath = (req.query['path'] as string) || (isWindows ? 'C:\\' : '/');
|
||||
const requestPath = getQueryStringParam(req.query['path']) || (isWindows ? process.env['USERPROFILE'] || 'C:\\' : '/');
|
||||
|
||||
let normalizedPath: string;
|
||||
try {
|
||||
@ -275,7 +291,7 @@ export const ReadFileHandler: RequestHandler = async (req, res) => {
|
||||
try {
|
||||
let filePath: string;
|
||||
try {
|
||||
filePath = normalizePath(req.query['path'] as string);
|
||||
filePath = normalizePath(getQueryStringParam(req.query['path']));
|
||||
} catch (pathError) {
|
||||
return sendError(res, '无效的文件路径');
|
||||
}
|
||||
@ -425,7 +441,7 @@ export const DownloadHandler: RequestHandler = async (req, res) => {
|
||||
try {
|
||||
let filePath: string;
|
||||
try {
|
||||
filePath = normalizePath(req.query['path'] as string);
|
||||
filePath = normalizePath(getQueryStringParam(req.query['path']));
|
||||
} catch (pathError) {
|
||||
return sendError(res, '无效的文件路径');
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user