From 7ec3cd15c45db8d54aa9af6d7653b47419fe37e4 Mon Sep 17 00:00:00 2001 From: SnowCat Security Bot Date: Fri, 13 Nov 2026 03:33:16 +0800 Subject: [PATCH 1/9] =?UTF-8?q?fix(security):=20=E8=84=B1=E6=95=8F?= =?UTF-8?q?=E5=90=AF=E5=8A=A8=E8=84=9A=E6=9C=AC=E4=B8=AD=E7=9A=84=E7=96=91?= =?UTF-8?q?=E4=BC=BC=E7=9C=9F=E5=AE=9EQQ=E5=8F=B7=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E5=80=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - snowcat-launch.ps1: -Account 默认值与用法注释改为 (对齐 release-tools 已脱敏版本) - snowcat-launch-9937.ps1: 启动参数 -q<号码> 改为 -q 占位 - 说明: 一次性验证脚本与稳定线启动器不应把真实账号写死入库 --- snowcat-launch-9937.ps1 | 2 +- snowcat-launch.ps1 | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/snowcat-launch-9937.ps1 b/snowcat-launch-9937.ps1 index 9e38443..aab086d 100644 --- a/snowcat-launch-9937.ps1 +++ b/snowcat-launch-9937.ps1 @@ -39,5 +39,5 @@ $env:NAPCAT_INJECT_PATH = Join-Path $NapcatDir 'NapCatWinBootHook.dll' $env:NAPCAT_MAIN_PATH = $mainJs Write-Host ("[OK] qqnt=" + $app.version + " build=" + $qqnt['buildVersion']) $logFile = Join-Path $NapcatDir ('snowcat-boot5-' + (Get-Date -Format 'HHmmss') + '.log') -Start-Process -FilePath (Join-Path $NapcatDir 'NapCatWinBootMain.exe') -ArgumentList ('"' + $QQPath + '"', '"' + (Join-Path $NapcatDir 'NapCatWinBootHook.dll') + '"', '-q3430961870') -WindowStyle Hidden -RedirectStandardError $logFile +Start-Process -FilePath (Join-Path $NapcatDir 'NapCatWinBootMain.exe') -ArgumentList ('"' + $QQPath + '"', '"' + (Join-Path $NapcatDir 'NapCatWinBootHook.dll') + '"', '-q') -WindowStyle Hidden -RedirectStandardError $logFile Write-Host 'launched' diff --git a/snowcat-launch.ps1 b/snowcat-launch.ps1 index 83046f0..59dc785 100644 --- a/snowcat-launch.ps1 +++ b/snowcat-launch.ps1 @@ -4,11 +4,11 @@ # ① 自动发现 QQ:注册表 → 常见路径 → D:/OneKey 便携 → 找不到给一键安装提示 # ② 自动读真实版本:versions/*/resources/app/package.json(废手工 qqnt.json 钉版) # ③ 自动生成 qqnt.json + loadNapCat.js 到运行时目录 -# 用法: .\snowcat-launch.ps1 [-Account 3430961870] [-NapcatDir C:\dsh\.tmp\napcat-shell] [-QQPath 自定义QQ.exe] +# 用法: .\snowcat-launch.ps1 [-Account ] [-NapcatDir C:\dsh\.tmp\napcat-shell] [-QQPath 自定义QQ.exe] # 2026-11-11 立项 · SnowCat Project param( - [string]$Account = '3430961870', + [string]$Account = '', [string]$NapcatDir = 'C:\dsh\.tmp\napcat-shell', [string]$QQPath = '', [switch]$InstallIfMissing From 2ecdc87e406e922e7ba7343ccdec3327bb36f8a3 Mon Sep 17 00:00:00 2001 From: SnowCat Security Bot Date: Fri, 13 Nov 2026 03:33:16 +0800 Subject: [PATCH 2/9] =?UTF-8?q?fix(security):=20webui.json=20=E5=A2=9E?= =?UTF-8?q?=E8=A1=A5=20allowOrigins=20=E9=BB=98=E8=AE=A4=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 与 WebUiConfigSchema 新增的 allowOrigins 字段对齐(CORS 白名单来源) - token 保持 'random'(启动时随机生成),配置文件本身不落弱口令 --- packages/snowcat-webui-backend/webui.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/snowcat-webui-backend/webui.json b/packages/snowcat-webui-backend/webui.json index 5d74c5e..7a9d981 100644 --- a/packages/snowcat-webui-backend/webui.json +++ b/packages/snowcat-webui-backend/webui.json @@ -7,5 +7,6 @@ "accessControlMode": "none", "ipWhitelist": [], "ipBlacklist": [], - "enableXForwardedFor": false + "enableXForwardedFor": false, + "allowOrigins": [] } \ No newline at end of file From 6f9d3cdb0f530fb45f1464c27920c88632491aec Mon Sep 17 00:00:00 2001 From: SnowCat Security Bot Date: Fri, 13 Nov 2026 03:33:30 +0800 Subject: [PATCH 3/9] =?UTF-8?q?fix(security):=20SignToken=20=E5=AF=86?= =?UTF-8?q?=E9=92=A5=E4=B8=8E=E5=AF=86=E7=A0=81=E5=93=88=E5=B8=8C=E5=8A=A0?= =?UTF-8?q?=E5=9B=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. HMAC 密钥: 未配置 NAPCAT_WEBUI_JWT_SECRET_KEY 时不再回落 Math.random() (可预测密钥=凭证可伪造), 改用 crypto.randomBytes(32) 强随机并稳定缓存。 行为变化: 未配置密钥时重启后所有已签发凭证失效, 用户需重新登录。 2. 密码哈希: generatePasswordHash 升级为 scrypt(随机盐16B, N=16384,r=8,p=1), 存储格式 scrypt$$。 3. comparePasswordHash 双格式兼容: 新格式逐盐 scrypt 校验; 旧格式(单轮 SHA-256 password+'.napcat')仍可校验通过, 保证存量凭证不失效、不丢功能语义。 建议用户改密码一次即迁移到 scrypt。 4. 凭证比较统一走 timingSafeEqual, 消除逐字节短路时序侧信道。 --- .../src/helper/SignToken.ts | 51 ++++++++++++++++--- 1 file changed, 43 insertions(+), 8 deletions(-) diff --git a/packages/snowcat-webui-backend/src/helper/SignToken.ts b/packages/snowcat-webui-backend/src/helper/SignToken.ts index 6888ad8..ec38e7c 100644 --- a/packages/snowcat-webui-backend/src/helper/SignToken.ts +++ b/packages/snowcat-webui-backend/src/helper/SignToken.ts @@ -1,8 +1,25 @@ import crypto from 'crypto'; +import { scryptSync, randomBytes, timingSafeEqual } from 'crypto'; import store from 'snowcat-common/src/store'; import type { WebUiCredentialJson, WebUiCredentialInnerJson } from '@/snowcat-webui-backend/src/types'; + +// 常量时间字符串比较,避免逐字节短路造成的时序侧信道 +function safeEqual (a: string, b: string): boolean { + const bufA = Buffer.from(a, 'utf8'); + const bufB = Buffer.from(b, 'utf8'); + if (bufA.length !== bufB.length) return false; + return timingSafeEqual(bufA, bufB); +} + export class AuthHelper { - private static readonly secretKey = process.env['NAPCAT_WEBUI_JWT_SECRET_KEY'] || Math.random().toString(36).slice(2); + // JWT/HMAC 签名密钥:优先环境变量;未配置时用 crypto 强随机生成并稳定缓存, + // 不再回落 Math.random()(可预测密钥=凭证可伪造)。 + // 注意:随机密钥在进程重启后失效,所有已签发凭证随之失效,用户需重新登录。 + private static readonly secretKey: string = (() => { + const envKey = process.env['NAPCAT_WEBUI_JWT_SECRET_KEY']; + if (envKey && envKey.length >= 16) return envKey; + return crypto.randomBytes(32).toString('hex'); + })(); private static readonly MAX_CREDENTIAL_VALID_SECONDS = 3600; public static getSecretKey (): string { @@ -37,7 +54,7 @@ export class AuthHelper { .createHmac('sha256', AuthHelper.secretKey) .update(jsonString, 'utf8') .digest('hex'); - return calculatedHmac === credentialJson.Hmac; + return safeEqual(calculatedHmac, credentialJson.Hmac); } catch (_error) { return false; } @@ -65,7 +82,7 @@ export class AuthHelper { const createdTimeSeconds = Math.floor(Number(credentialJson.Data.CreatedTime)); const timeDifferenceSeconds = currentTimeSeconds - createdTimeSeconds; const isWithinOneHour = timeDifferenceSeconds >= 0 && timeDifferenceSeconds <= AuthHelper.MAX_CREDENTIAL_VALID_SECONDS; - return isWithinOneHour && credentialJson.Data.HashEncoded === AuthHelper.generatePasswordHash(token); + return isWithinOneHour && safeEqual(credentialJson.Data.HashEncoded, AuthHelper.generatePasswordHash(token)); } /** @@ -94,21 +111,39 @@ export class AuthHelper { } /** - * 生成密码Hash + * 生成密码Hash(scrypt,随机盐,N=16384 r=8 p=1)。 + * 存储格式: scrypt$$ * @param password 密码 - * @returns 生成的Hash值 + * @returns 生成的Hash值(含盐与参数,可自校验) */ public static generatePasswordHash (password: string): string { - return crypto.createHash('sha256').update(password + '.napcat').digest().toString('hex'); + const salt = randomBytes(16); + const hash = scryptSync(password, salt, 32, { N: 16384, r: 8, p: 1 }); + return `scrypt$${salt.toString('hex')}$${hash.toString('hex')}`; } /** - * 对比密码和Hash值 + * 对比密码和Hash值。 + * 兼容两种输入: + * - 新格式 scrypt$$(逐盐校验,恒定时间比较) + * - 旧格式 单轮 SHA-256 hex(历史凭证,仅供校验,不用于生成) * @param password 密码 * @param hash Hash值 * @returns 布尔值,表示密码是否匹配Hash值 */ public static comparePasswordHash (password: string, hash: string): boolean { - return this.generatePasswordHash(password) === hash; + if (hash.startsWith('scrypt$')) { + const parts = hash.split('$'); + if (parts.length !== 3) return false; + const salt = Buffer.from(parts[1] ?? '', 'hex'); + const expected = Buffer.from(parts[2] ?? '', 'hex'); + if (salt.length !== 16 || expected.length !== 32) return false; + const actual = scryptSync(password, salt, 32, { N: 16384, r: 8, p: 1 }); + return timingSafeEqual(actual, expected); + } + // 兼容历史数据:旧版单轮 SHA-256(password + '.napcat') + // 老凭证仍可通过校验(不丢功能语义),升级后建议改密码以迁移到 scrypt。 + const legacy = crypto.createHash('sha256').update(password + '.napcat').digest('hex'); + return safeEqual(legacy, hash); } } From fe5dafc527820220e9fe849043d0d77f8cc9058a Mon Sep 17 00:00:00 2001 From: SnowCat Security Bot Date: Fri, 13 Nov 2026 03:33:30 +0800 Subject: [PATCH 4/9] =?UTF-8?q?fix(security):=20CORS=20=E4=B8=8D=E5=86=8D?= =?UTF-8?q?=E5=8F=8D=E5=B0=84=E4=BB=BB=E6=84=8F=20Origin;=20X-Forwarded-Fo?= =?UTF-8?q?r=20=E6=94=B9=E8=B5=B0=20trust=20proxy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. cors.ts: 不再把任意请求 Origin 原样反射为 Access-Control-Allow-Origin (配合 credentials:true = 任意站点可携带 Cookie 跨域调用管理 API)。 新策略: 仅放行 a) 无 Origin 头(同源/直连) b) Origin host 与请求 host 一致 (本机多端口开发) c) webui.json allowOrigins 精确白名单。 新增 Vary: Origin。 2. IP 访问控制: 客户端 IP 不再读取客户端可伪造的 X-Forwarded-For 头, 统一走 req.ip(express 'trust proxy' 控制可信代理)。enableXForwardedFor 字段保留(向后兼容)但不再影响 IP 判定, 需要代理真实 IP 的场景应配置 app.set('trust proxy')。 3. WebUiConfigSchema 新增 allowOrigins: string[] (default []), 与前端 GetConfig/UpdateConfig 同步支持。 回归注意: 若部署依赖旧反射行为的前端跨域, 需在 webui.json 配 allowOrigins。 --- .../src/helper/config.ts | 3 ++ .../src/middleware/cors.ts | 40 ++++++++----------- 2 files changed, 20 insertions(+), 23 deletions(-) diff --git a/packages/snowcat-webui-backend/src/helper/config.ts b/packages/snowcat-webui-backend/src/helper/config.ts index 7befb4e..9d0eb19 100644 --- a/packages/snowcat-webui-backend/src/helper/config.ts +++ b/packages/snowcat-webui-backend/src/helper/config.ts @@ -32,7 +32,10 @@ const WebUiConfigSchema = Type.Object({ // IP黑名单列表 ipBlacklist: Type.Array(Type.String(), { default: [] }), // 是否启用 X-Forwarded-For 获取真实IP + // 安全提示:该请求头可被客户端伪造,仅在部署于可信反向代理(会重写该头)之后才有意义。 enableXForwardedFor: Type.Boolean({ default: false }), + // 允许跨域访问的前端源列表(精确匹配,含协议)。空列表 = 仅允许同源/本机 host 一致的 Origin。 + allowOrigins: Type.Array(Type.String(), { default: [] }), // 是否启用双因素认证(2FA) enable2FA: Type.Boolean({ default: false }), // TOTP 密钥 diff --git a/packages/snowcat-webui-backend/src/middleware/cors.ts b/packages/snowcat-webui-backend/src/middleware/cors.ts index 3505d7c..037df34 100644 --- a/packages/snowcat-webui-backend/src/middleware/cors.ts +++ b/packages/snowcat-webui-backend/src/middleware/cors.ts @@ -184,24 +184,9 @@ function isIPInList (ip: string, ipList: string[]): boolean { export const cors: RequestHandler = async (req, res, next) => { const config = await WebUiConfig.GetWebUIConfig(); - // 根据配置决定如何获取客户端IP - let clientIP: string; - if (config.enableXForwardedFor) { - // 启用 X-Forwarded-For 时,优先从该头部获取真实IP - const forwardedFor = req.headers['x-forwarded-for']; - if (typeof forwardedFor === 'string') { - // X-Forwarded-For 可能包含多个IP,取第一个 - clientIP = forwardedFor.split(',')[0]?.trim() || ''; - } else if (Array.isArray(forwardedFor) && forwardedFor.length > 0) { - clientIP = forwardedFor[0] || ''; - } else { - // 如果没有 X-Forwarded-For,回退到 socket IP - clientIP = req.ip || req.socket.remoteAddress || ''; - } - } else { - // 默认使用 socket IP - clientIP = req.ip || req.socket.remoteAddress || ''; - } + // 获取客户端IP优先走 req.ip(express 'trust proxy' 负责可信代理的解析), + // 不再无条件信任请求方可伪造的 X-Forwarded-For 头。 + const clientIP: string = req.ip || req.socket.remoteAddress || ''; // 检查访问控制模式 if (config.accessControlMode === 'whitelist') { @@ -219,11 +204,20 @@ export const cors: RequestHandler = async (req, res, next) => { } // 'none' 模式:不进行任何限制 - const origin = req.headers.origin || '*'; - res.header('Access-Control-Allow-Origin', origin); - res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); - res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization'); - res.header('Access-Control-Allow-Credentials', 'true'); + // CORS:不再反射任意 Origin。 + // 未配置 allowOrigins 时,只有「无 Origin 头(同源/直连工具)」或「Origin 主机与请求 host 一致(本地多端口开发)」才放行跨域头, + // 其余一律不带 ACAO 头(浏览器会拒绝跨域响应)。 + const origin = req.headers.origin; + const requestHost = req.headers.host || ''; + const originHost = typeof origin === 'string' && origin.startsWith('http') ? origin.replace(/^https?:\/\//, '').split('/')[0] ?? '' : ''; + const hostMatches = originHost !== '' && originHost === requestHost; + if (origin && (config.allowOrigins?.length ? config.allowOrigins.includes(origin) : hostMatches)) { + res.header('Access-Control-Allow-Origin', origin); + res.header('Vary', 'Origin'); + res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); + res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization'); + res.header('Access-Control-Allow-Credentials', 'true'); + } if (req.method === 'OPTIONS') { res.sendStatus(204); From 030ecb206ccac4cc5d63ca24ce7d66850e0e04f6 Mon Sep 17 00:00:00 2001 From: SnowCat Security Bot Date: Fri, 13 Nov 2026 03:33:44 +0800 Subject: [PATCH 5/9] =?UTF-8?q?fix(security):=20SSL=20=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E4=B8=8D=E5=86=8D=E5=9B=9E=E5=90=90=E8=AF=81?= =?UTF-8?q?=E4=B9=A6=E4=B8=8E=E7=A7=81=E9=92=A5=E6=98=8E=E6=96=87;=20?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E6=8E=A5=E5=8F=A3=E5=89=94=E9=99=A4=E6=95=8F?= =?UTF-8?q?=E6=84=9F=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. GetSSLStatusHandler: 移除 certContent/keyContent 字段, 只返回 certExists/keyExists/enabled 存在性(前端展示用途足够)。 TLS 私钥不应经 HTTP API 下发, 即使是已登录管理员会话也应避免。 2. GetWebUIConfigHandler: 不回吐 token/totpSecret 等敏感字段。 3. UpdateWebUIConfigHandler: 支持 allowOrigins 数组校验与更新。 回归注意: 前端如有依赖 certContent 做展示/编辑的组件, 改为只读存在性; SSL 证书内容的查看走服务器文件路径(cert.pem/key.pem)。 --- .../src/api/WebUIConfig.ts | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/packages/snowcat-webui-backend/src/api/WebUIConfig.ts b/packages/snowcat-webui-backend/src/api/WebUIConfig.ts index 2944609..06fb12b 100644 --- a/packages/snowcat-webui-backend/src/api/WebUIConfig.ts +++ b/packages/snowcat-webui-backend/src/api/WebUIConfig.ts @@ -18,6 +18,8 @@ export const GetWebUIConfigHandler: RequestHandler = async (_, res) => { ipWhitelist: config.ipWhitelist || [], ipBlacklist: config.ipBlacklist || [], enableXForwardedFor: config.enableXForwardedFor || false, + allowOrigins: config.allowOrigins || [], + // 安全修复:不回吐 token / totpSecret 等敏感字段 }); } catch (error) { const msg = (error as Error).message; @@ -93,7 +95,7 @@ export const GetClientIPHandler: RequestHandler = async (req, res) => { // 更新WebUI基础配置 export const UpdateWebUIConfigHandler: RequestHandler = async (req, res) => { try { - const { host, port, loginRate, disableWebUI, accessControlMode, ipWhitelist, ipBlacklist, enableXForwardedFor } = req.body; + const { host, port, loginRate, disableWebUI, accessControlMode, ipWhitelist, ipBlacklist, enableXForwardedFor, allowOrigins } = req.body; const updateConfig: any = {}; @@ -153,6 +155,13 @@ export const UpdateWebUIConfigHandler: RequestHandler = async (req, res) => { updateConfig.enableXForwardedFor = enableXForwardedFor; } + if (allowOrigins !== undefined) { + if (!Array.isArray(allowOrigins)) { + return sendError(res, 'allowOrigins必须是数组'); + } + updateConfig.allowOrigins = allowOrigins; + } + await WebUiConfig.UpdateWebUIConfig(updateConfig); return sendSuccess(res, null); } catch (error) { @@ -161,7 +170,7 @@ export const UpdateWebUIConfigHandler: RequestHandler = async (req, res) => { } }; -// 获取SSL证书状态 +// 获取SSL证书状态(只返回存在性,不回吐证书与私钥内容) export const GetSSLStatusHandler: RequestHandler = async (_, res) => { try { const certPath = join(webUiPathWrapper.configPath, 'cert.pem'); @@ -170,22 +179,12 @@ export const GetSSLStatusHandler: RequestHandler = async (_, res) => { const certExists = existsSync(certPath); const keyExists = existsSync(keyPath); - let certContent = ''; - let keyContent = ''; - - if (certExists) { - certContent = await fsProm.readFile(certPath, 'utf-8'); - } - if (keyExists) { - keyContent = await fsProm.readFile(keyPath, 'utf-8'); - } - + // 安全修复:不再返回 certContent / keyContent(TLS 私钥明文不应经 API 下发)。 + // 前端只需展示「已配置/未配置」状态;如需查看证书详情可自行在服务器上读取。 return sendSuccess(res, { enabled: certExists && keyExists, certExists, keyExists, - certContent, - keyContent, }); } catch (error) { const msg = (error as Error).message; From 76520c6157086340fc75f7a6d5e7e533e2bb20ee Mon Sep 17 00:00:00 2001 From: SnowCat Security Bot Date: Fri, 13 Nov 2026 03:33:45 +0800 Subject: [PATCH 6/9] =?UTF-8?q?fix(security):=20=E7=A1=AC=E7=BC=96?= =?UTF-8?q?=E7=A0=81=E7=89=B9=E6=AE=8A=E8=B4=A6=E5=8F=B7=E6=94=B9=E4=B8=BA?= =?UTF-8?q?=E5=8F=AF=E9=85=8D=E7=BD=AE=E6=95=B0=E7=BB=84=20NAPCAT=5FSPECIA?= =?UTF-8?q?L=5FPEER=5FUINS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - onebot/api/msg.ts(3处)与 core/helper/log.ts(1处)原先写死两个特定 peerUin, 用于: 回复消息直接返回缓存的 createReplyData / 走合并视频URL 特殊通道 / 回复元素跳过递归解析 / 临时会话兜底 group_id。 - 改为环境变量 NAPCAT_SPECIAL_PEER_UINS(逗号分隔, 校验纯数字)读取, 默认为空数组 = 不做任何特殊处理。 - 如需恢复旧行为: NAPCAT_SPECIAL_PEER_UINS='<原两个号码>' 即可。 回归注意: 默认关闭特殊通道后, 对这两类会话的合并转发视频URL获取与回复 缓存直返行为会走通用分支, 建议内部测试确认无回归后按需配置环境变量。 --- packages/snowcat-core/helper/log.ts | 9 ++++++++- packages/snowcat-onebot/api/msg.ts | 14 +++++++++++--- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/packages/snowcat-core/helper/log.ts b/packages/snowcat-core/helper/log.ts index e16a743..ecd4fb0 100644 --- a/packages/snowcat-core/helper/log.ts +++ b/packages/snowcat-core/helper/log.ts @@ -336,9 +336,16 @@ function textElementToText (textElement: any): string { return ''; } +// 历史: 同 onebot/api/msg.ts 的特定会话特殊处理, 改为可配置数组, 默认空=不跳过。 +// 如需旧行为: NAPCAT_SPECIAL_PEER_UINS='284840486,1094950020' +const SPECIAL_PEER_UINS: string[] = (process.env['NAPCAT_SPECIAL_PEER_UINS'] ?? '') + .split(',') + .map(s => s.trim()) + .filter(s => /^\d+$/.test(s)); + function replyElementToText (replyElement: any, msg: RawMessage, recursiveLevel: number): string { const recordMsgOrNull = msg.records.find((record) => replyElement.sourceMsgIdInRecords === record.msgId); - return `[回复消息 ${recordMsgOrNull && recordMsgOrNull.peerUin !== '284840486' && recordMsgOrNull.peerUin !== '1094950020' + return `[回复消息 ${recordMsgOrNull && !SPECIAL_PEER_UINS.includes(recordMsgOrNull.peerUin) ? rawMessageToText(recordMsgOrNull, recursiveLevel + 1) : `未找到消息记录 (MsgId = ${replyElement.sourceMsgIdInRecords})` }]`; diff --git a/packages/snowcat-onebot/api/msg.ts b/packages/snowcat-onebot/api/msg.ts index efa3f37..df179b4 100644 --- a/packages/snowcat-onebot/api/msg.ts +++ b/packages/snowcat-onebot/api/msg.ts @@ -1,4 +1,12 @@ import { FileNapCatOneBotUUID } from 'snowcat-common/src/file-uuid'; + +// 历史: 特定会话(如文件传输助手/官方助手类号)走特殊回包与视频URL通道。 +// 改造: 从硬编码字面量改为可配置数组(环境变量优先, 默认空=不做特殊处理), 保留原分支结构。 +// 如需恢复旧行为, 配置 NAPCAT_SPECIAL_PEER_UINS='284840486,1094950020' 即可。 +const SPECIAL_PEER_UINS: string[] = (process.env['NAPCAT_SPECIAL_PEER_UINS'] ?? '') + .split(',') + .map(s => s.trim()) + .filter(s => /^\d+$/.test(s)); import { MessageUnique } from 'snowcat-common/src/message-unique'; import { ChatType, @@ -298,7 +306,7 @@ export class OneBotMsgApi { const records = msg.records.find(msgRecord => msgRecord.msgId === element?.sourceMsgIdInRecords); // 特定账号的特殊处理 - if (records && (records.peerUin === '284840486' || records.peerUin === '1094950020')) { + if (records && (SPECIAL_PEER_UINS.includes(records.peerUin))) { return createReplyData(records.msgId); } @@ -416,7 +424,7 @@ export class OneBotMsgApi { // 读取视频链接并兜底 let videoUrlWrappers: Awaited> | undefined; - if (msg.peerUin === '284840486' || msg.peerUin === '1094950020') { + if (SPECIAL_PEER_UINS.includes(msg.peerUin)) { try { videoUrlWrappers = await this.core.apis.FileApi.getVideoUrl({ chatType: msg.chatType, @@ -1171,7 +1179,7 @@ export class OneBotMsgApi { resMsg.sender.nickname = member?.nick ?? member?.cardName ?? '临时会话'; resMsg.temp_source = 0; } else { - resMsg.group_id = 284840486; + resMsg.group_id = SPECIAL_PEER_UINS[0] ? parseInt(SPECIAL_PEER_UINS[0]) : 0; resMsg.temp_source = 0; resMsg.sender.nickname = '临时会话'; } From caedbb219dfa85fac54b5e27c577d1d35d3e1a4b Mon Sep 17 00:00:00 2001 From: SnowCat Security Bot Date: Fri, 13 Nov 2026 03:33:45 +0800 Subject: [PATCH 7/9] =?UTF-8?q?fix(security):=20=E6=97=A5=E5=BF=97?= =?UTF-8?q?=E8=84=B1=E6=95=8F=E8=A7=84=E5=88=99=E5=A2=9E=E5=BC=BA,=20?= =?UTF-8?q?=E8=A6=86=E7=9B=96=E6=9B=B4=E5=A4=9A=20token=20=E6=B3=84?= =?UTF-8?q?=E9=9C=B2=E5=BD=A2=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 原 sanitizeLog 仅匹配 token=[\w\d]+, 无法覆盖 token: xxx / access_token / refresh_token / credential / Authorization 及含 ./- 的 JWT 形态。 - 扩展为三类规则: token=形式 / token: 形式 / 敏感凭据字段名前缀。 - 同时覆盖 LogHandler(历史日志)与 LogRealTimeHandler(SSE 实时日志), 两处本就共用 sanitizeLog, 无需改动调用侧。 --- packages/snowcat-webui-backend/src/api/Log.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/snowcat-webui-backend/src/api/Log.ts b/packages/snowcat-webui-backend/src/api/Log.ts index 40db251..c802938 100644 --- a/packages/snowcat-webui-backend/src/api/Log.ts +++ b/packages/snowcat-webui-backend/src/api/Log.ts @@ -7,8 +7,11 @@ const isMacOS = process.platform === 'darwin'; // 日志脱敏函数 const sanitizeLog = (log: string): string => { - // 脱敏 token 参数,将 token=xxx 替换为 token=*** - return log.replace(/token=[\w\d]+/gi, 'token=***'); + // 脱敏: token=xxx / token: xxx / access_token / refresh_token / credential / Authorization 一律打码 + return log + .replace(/token=[\w\d.\-]+/gi, 'token=***') + .replace(/token[:=]\s*"?[\w\d.\-]+/gi, 'token=***') + .replace(/(access_token|refresh_token|credential|Authorization)[:=]\s*"?[\w\d.\-+/=]+/gi, '$1=***'); }; // 日志记录 export const LogHandler: RequestHandler = async (req, res) => { From 148db8db87475f5b41336fdc869a3787f01cd93a Mon Sep 17 00:00:00 2001 From: SnowCat Security Bot Date: Fri, 13 Nov 2026 03:34:04 +0800 Subject: [PATCH 8/9] =?UTF-8?q?fix(security):=20=E6=B3=A8=E9=87=8A?= =?UTF-8?q?=E4=B8=AD=E7=9A=84=E7=A4=BA=E4=BE=8B=E5=8F=B7=E7=A0=81=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=E8=84=B1=E6=95=8F=E4=B8=BA=20?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/snowcat-core/helper/log.ts | 2 +- packages/snowcat-onebot/api/msg.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/snowcat-core/helper/log.ts b/packages/snowcat-core/helper/log.ts index ecd4fb0..81c784a 100644 --- a/packages/snowcat-core/helper/log.ts +++ b/packages/snowcat-core/helper/log.ts @@ -337,7 +337,7 @@ function textElementToText (textElement: any): string { } // 历史: 同 onebot/api/msg.ts 的特定会话特殊处理, 改为可配置数组, 默认空=不跳过。 -// 如需旧行为: NAPCAT_SPECIAL_PEER_UINS='284840486,1094950020' +// 如需旧行为: NAPCAT_SPECIAL_PEER_UINS=',' const SPECIAL_PEER_UINS: string[] = (process.env['NAPCAT_SPECIAL_PEER_UINS'] ?? '') .split(',') .map(s => s.trim()) diff --git a/packages/snowcat-onebot/api/msg.ts b/packages/snowcat-onebot/api/msg.ts index df179b4..ea8f102 100644 --- a/packages/snowcat-onebot/api/msg.ts +++ b/packages/snowcat-onebot/api/msg.ts @@ -2,7 +2,7 @@ import { FileNapCatOneBotUUID } from 'snowcat-common/src/file-uuid'; // 历史: 特定会话(如文件传输助手/官方助手类号)走特殊回包与视频URL通道。 // 改造: 从硬编码字面量改为可配置数组(环境变量优先, 默认空=不做特殊处理), 保留原分支结构。 -// 如需恢复旧行为, 配置 NAPCAT_SPECIAL_PEER_UINS='284840486,1094950020' 即可。 +// 如需恢复旧行为, 配置 NAPCAT_SPECIAL_PEER_UINS=',' 即可。 const SPECIAL_PEER_UINS: string[] = (process.env['NAPCAT_SPECIAL_PEER_UINS'] ?? '') .split(',') .map(s => s.trim()) From 5c98cc4a25379e923b47d279cc309ac5cf572e59 Mon Sep 17 00:00:00 2001 From: SnowCat Security Bot Date: Fri, 13 Nov 2026 03:34:50 +0800 Subject: [PATCH 9/9] =?UTF-8?q?fix(security):=20go-cqhttp=20=E5=85=BC?= =?UTF-8?q?=E5=AE=B9=E5=B1=82=E5=8D=A0=E4=BD=8D=E6=B6=88=E6=81=AF=E7=9A=84?= =?UTF-8?q?=E5=86=99=E6=AD=BB=E8=B4=A6=E5=8F=B7=E5=90=8C=E6=A0=B7=E6=94=B9?= =?UTF-8?q?=E8=B5=B0=20NAPCAT=5FSPECIAL=5FPEER=5FUINS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GetForwardMsg.ts 构造 fakeMsg 时的 peerUid/peerUin/senderUin 占位值 改为读同一环境变量数组, 默认空字符串。 - release-tools/build/napcat.mjs 为构建产物, 由下次构建自动重生成, 不手改。 回归注意: 默认占位为空串, go-cqhttp 兼容转发查询在未配置该变量时返回的 占位主体字段为空, 如下游依赖非空占位可配置环境变量恢复。 --- .../action/go-cqhttp/GetForwardMsg.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/snowcat-onebot/action/go-cqhttp/GetForwardMsg.ts b/packages/snowcat-onebot/action/go-cqhttp/GetForwardMsg.ts index 4f81c99..a3baacf 100644 --- a/packages/snowcat-onebot/action/go-cqhttp/GetForwardMsg.ts +++ b/packages/snowcat-onebot/action/go-cqhttp/GetForwardMsg.ts @@ -7,6 +7,13 @@ import { ChatType, ElementType, MsgSourceType, NTMsgType, RawMessage } from 'sno import { isNumeric } from 'snowcat-common/src/helper'; import { GoCQHTTPActionsExamples } from '../example/GoCQHTTPActionsExamples'; + +// 历史: 兼容层构造的占位消息主体。与 NAPCAT_SPECIAL_PEER_UINS 统一配置。 +const SPECIAL_PEER_UINS: string[] = (process.env['NAPCAT_SPECIAL_PEER_UINS'] ?? '') + .split(',') + .map(s => s.trim()) + .filter(s => /^\d+$/.test(s)); + const PayloadSchema = Type.Object({ message_id: Type.Optional(Type.String({ description: '消息ID' })), id: Type.Optional(Type.String({ description: '消息ID' })), @@ -106,14 +113,14 @@ export class GoCQHTTPGetForwardMsgAction extends OneBotAction