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