From 01823a1db47d839ef416945b42d9934211a4eb6f Mon Sep 17 00:00:00 2001 From: xcbyy <2578898740@qq.com> Date: Mon, 19 Oct 2026 10:33:31 +0800 Subject: [PATCH] =?UTF-8?q?security:=20=E7=A7=BB=E9=99=A4=E7=A1=AC?= =?UTF-8?q?=E7=BC=96=E7=A0=81=E5=BE=AE=E4=BF=A1=E5=B0=8F=E7=A8=8B=E5=BA=8F?= =?UTF-8?q?=20appId/appSecret=EF=BC=8C=E6=94=B9=E4=B8=BA=E5=8F=AA=E8=AF=BB?= =?UTF-8?q?=E9=85=8D=E7=BD=AE+=E7=BC=BA=E5=A4=B1=E5=8D=B3=E5=A4=B1?= =?UTF-8?q?=E8=B4=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端不再兜底内置 appId/appSecret,缺失时 jscode2session/手机号/支付/内容审核等接口明确拒绝(fail-closed) - 建表种子改为空值;启动自愈由「写入占位值」改为「清空历史占位值」,杜绝误用非本小程序 appId(40037 根因) - 后台/引导文案去除具体 AppID;文档补充 WX_APP_ID/WX_APP_SECRET 说明 - 新增回归护栏 wechat-appid-config-hardening.spec.ts(87 套件/636 用例全绿);HTTP 冒烟 4/4 验证缺失即失败 --- .../src/views/finance/config/index.vue | 2 +- .../views/safety/platform-config/index.vue | 8 ++-- docs/部署验收与1分钱支付闭环.md | 5 +++ server/src/db/mysql.ts | 16 ++++---- server/src/index.ts | 33 +++++++-------- .../wechat-appid-config-hardening.spec.ts | 41 +++++++++++++++++++ 6 files changed, 77 insertions(+), 28 deletions(-) create mode 100644 server/tests/wechat-appid-config-hardening.spec.ts diff --git a/admin/arco-design-pro-vite/src/views/finance/config/index.vue b/admin/arco-design-pro-vite/src/views/finance/config/index.vue index a6ce8eb..7506d2b 100644 --- a/admin/arco-design-pro-vite/src/views/finance/config/index.vue +++ b/admin/arco-design-pro-vite/src/views/finance/config/index.vue @@ -150,7 +150,7 @@ diff --git a/admin/arco-design-pro-vite/src/views/safety/platform-config/index.vue b/admin/arco-design-pro-vite/src/views/safety/platform-config/index.vue index 4a269d0..68a7eed 100644 --- a/admin/arco-design-pro-vite/src/views/safety/platform-config/index.vue +++ b/admin/arco-design-pro-vite/src/views/safety/platform-config/index.vue @@ -796,7 +796,7 @@ - + @@ -974,7 +974,7 @@ 在此维护微信小程序的地图定位服务参数(腾讯位置服务 Tencent LBS Key)。用于同城组局发布时的地图选点(chooseLocation)、经纬度逆地址解析及附近搭子/活动测距。
- 配置指引:登录腾讯位置服务控制台 (lbs.qq.com) -> 创建应用并添加 Key -> 勾选 WebServiceAPI 与微信小程序授权(填入小程序 AppID: wx0c929f7e54024e67)。 + 配置指引:登录腾讯位置服务控制台 (lbs.qq.com) -> 创建应用并添加 Key -> 勾选 WebServiceAPI 与微信小程序授权(填入你的小程序 AppID)。
@@ -1002,7 +1002,7 @@ allow-clear /> @@ -1504,7 +1504,7 @@ const mapLocationForm = reactive({ enableChooseLocation: 1, enableDistanceCalculation: 1, defaultCity: '北京市', - remark: '腾讯位置服务 Key(需在 lbs.qq.com 申请并勾选 WebServiceAPI 与微信小程序 AppID: wx0c929f7e54024e67)' + remark: '腾讯位置服务 Key(需在 lbs.qq.com 申请并勾选 WebServiceAPI 与微信小程序)' } }); diff --git a/docs/部署验收与1分钱支付闭环.md b/docs/部署验收与1分钱支付闭环.md index 4c7844e..a0e8f1f 100644 --- a/docs/部署验收与1分钱支付闭环.md +++ b/docs/部署验收与1分钱支付闭环.md @@ -28,6 +28,11 @@ MYSQL_DATABASE=peiwan # 可选:跨域白名单(逗号分隔;默认仅同源可用,小程序/服务端调用不受影响) # CORS_ORIGINS=https://admin.example.com,https://h5.example.com +# 微信小程序凭证(无内置兜底值:不配则"微信登录换 openid / 内容审核 / 订阅消息 / 支付"相关接口直接拒绝) +# 也可在后台「平台配置 → 微信支付商户配置」填写(后台优先于环境变量) +# WX_APP_ID=wx**************** +# WX_APP_SECRET=<32位小程序 AppSecret> + # 可选:微信(若已在后台"微信支付配置"页填写,则无需在这里重复;后台配置优先) # WX_APP_ID=... # WX_APP_SECRET=... diff --git a/server/src/db/mysql.ts b/server/src/db/mysql.ts index 08c78a0..72a7c87 100644 --- a/server/src/db/mysql.ts +++ b/server/src/db/mysql.ts @@ -1034,8 +1034,8 @@ async function seedInitialDataIfEmpty(conn: any): Promise { const wechatPayConfigVal = JSON.stringify({ mode: 'AUTO', - appId: 'wx0c929f7e54024e67', - appSecret: '63a412f3f73af8e73f6923943bf9683c', + appId: '', + appSecret: '', mchId: '', apiV3Key: '', serialNo: '', @@ -1055,7 +1055,7 @@ async function seedInitialDataIfEmpty(conn: any): Promise { enableChooseLocation: 1, enableDistanceCalculation: 1, defaultCity: '北京市', - remark: '腾讯位置服务 Key(需在 lbs.qq.com 申请并勾选 WebServiceAPI 与微信小程序 AppID: wx0c929f7e54024e67)' + remark: '腾讯位置服务 Key(需在 lbs.qq.com 申请并勾选 WebServiceAPI 与微信小程序)' }); const [cfgItemRows]: any = await conn.query('SELECT COUNT(*) as count FROM platform_configs'); @@ -1197,12 +1197,14 @@ async function seedInitialDataIfEmpty(conn: any): Promise { } if (curVal && typeof curVal === 'object') { let updated = false; - if (!curVal.appId || curVal.appId === 'wx_peiwan_miniapp') { - curVal.appId = 'wx0c929f7e54024e67'; + // 历史版本曾在源码/DB 中内置占位 appId/appSecret,这里一次性清空, + // 避免后端误用非本小程序的凭证(如订阅消息 40037、支付参数错乱)。 + if (curVal.appId === 'wx0c929f7e54024e67' || curVal.appId === 'wx_peiwan_miniapp') { + curVal.appId = ''; updated = true; } - if (!curVal.appSecret) { - curVal.appSecret = '63a412f3f73af8e73f6923943bf9683c'; + if (curVal.appSecret === '63a412f3f73af8e73f6923943bf9683c') { + curVal.appSecret = ''; updated = true; } if (updated) { diff --git a/server/src/index.ts b/server/src/index.ts index 9f92b27..fe790a7 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -1922,8 +1922,8 @@ async function resolveWechatPayRuntimeConfig(overrideCfg?: any) { const merged = { mode: 'AUTO', - appId: process.env.WX_APP_ID || 'wx0c929f7e54024e67', - appSecret: process.env.WX_APP_SECRET || '63a412f3f73af8e73f6923943bf9683c', + appId: process.env.WX_APP_ID || '', + appSecret: process.env.WX_APP_SECRET || '', mchId: process.env.WX_MCH_ID || '', apiV3Key: process.env.WX_API_V3_KEY || '', serialNo: process.env.WX_SERIAL_NO || '', @@ -2211,11 +2211,11 @@ app.post('/api/wechat/code2session', async (req: Request, res: Response) => { } const cfg = await resolveWechatPayRuntimeConfig(); - const appId = cfg.appId || 'wx0c929f7e54024e67'; - const appSecret = cfg.appSecret || '63a412f3f73af8e73f6923943bf9683c'; + const appId = String(cfg.appId || '').trim(); + const appSecret = String(cfg.appSecret || '').trim(); - if (!appSecret) { - return res.status(400).json({ code: 40000, message: '服务端未配置小程序 AppSecret' }); + if (!appId || !appSecret) { + return res.status(400).json({ code: 40000, message: '服务端未配置小程序 AppID/AppSecret,无法换取 openid' }); } const wxUrl = `https://api.weixin.qq.com/sns/jscode2session?appid=${encodeURIComponent(appId)}&secret=${encodeURIComponent(appSecret)}&js_code=${encodeURIComponent(code)}&grant_type=authorization_code`; @@ -2405,17 +2405,15 @@ setWeChatRefundFn(async (orderId, refundCents, totalCents, reason, outRefundNo) // 注册微信官方内容安全 msgSecCheck(v2) 真实检测器: // 本地词库先做即时拦截,本地放行后再交由微信官方机器审核做第二层真实防御。 -// 未配置真实小程序凭证(仍为内置占位值)或显式关闭时,自动跳过远端并降级本地词库。 +// 未配置真实小程序凭证时(appId/appSecret 为空),自动跳过远端并降级本地词库。 contentSecurityService.setRemoteChecker(async (text: string, openid?: string, scene?: number) => { try { const cfg = await resolveWechatPayRuntimeConfig(); const appId = String(cfg.appId || '').trim(); const appSecret = String(cfg.appSecret || '').trim(); - const PLACEHOLDER_APP_ID = 'wx0c929f7e54024e67'; - const PLACEHOLDER_APP_SECRET = '63a412f3f73af8e73f6923943bf9683c'; - const forced = process.env.WX_CONTENT_SEC_CHECK === '1'; + + // 仅当显式配置了真实小程序凭证时才调用官方内容审核(不再有内置占位值兜底) if (!appId || !appSecret) return null; - if (!forced && (appId === PLACEHOLDER_APP_ID || appSecret === PLACEHOLDER_APP_SECRET)) return null; const accessToken = await getWechatAccessToken(appId, appSecret); if (!accessToken) return null; @@ -2472,11 +2470,11 @@ app.post('/api/auth/wechat-phone', async (req: Request, res: Response) => { } const cfg = await resolveWechatPayRuntimeConfig(); - const appId = cfg.appId || 'wx0c929f7e54024e67'; - const appSecret = cfg.appSecret || '63a412f3f73af8e73f6923943bf9683c'; + const appId = String(cfg.appId || '').trim(); + const appSecret = String(cfg.appSecret || '').trim(); - if (!appSecret) { - return res.status(400).json({ code: 40000, message: '服务端未配置小程序 AppSecret' }); + if (!appId || !appSecret) { + return res.status(400).json({ code: 40000, message: '服务端未配置小程序 AppID/AppSecret' }); } // 1. 若前端传递了 wxCode,顺便获取当前用户的真实 openid @@ -2607,7 +2605,10 @@ app.post('/api/pay/wechat/prepay', async (req: Request, res: Response) => { } const cfg = await resolveWechatPayRuntimeConfig(); - const appId = cfg.appId || 'wx0c929f7e54024e67'; + const appId = String(cfg.appId || '').trim(); + if (!appId) { + return res.status(400).json({ code: 40000, message: '服务端未配置小程序 AppID(后台「微信支付商户配置」或环境变量 WX_APP_ID),无法发起微信支付' }); + } const outTradeNo = orderId ? String(orderId) : `WX${scene === 'RECHARGE' ? 'R' : 'O'}${Date.now()}${uid}`; const desc = title || (scene === 'RECHARGE' ? '钱包余额充值' : '同城陪伴担保订单'); diff --git a/server/tests/wechat-appid-config-hardening.spec.ts b/server/tests/wechat-appid-config-hardening.spec.ts new file mode 100644 index 0000000..d523fe8 --- /dev/null +++ b/server/tests/wechat-appid-config-hardening.spec.ts @@ -0,0 +1,41 @@ +// 已读取AGENTS.md +import * as fs from 'fs'; +import * as path from 'path'; + +/** + * 第七十一批回归闸门:移除硬编码微信小程序 appId/appSecret,改为"只读配置 + 缺失即失败"。 + * 背景:源码/DB 内置占位凭证会被后端静默使用,导致 appId 与建模板的小程序不一致(订阅消息 40037)。 + */ +describe('第七十一批小程序凭证去硬编码闸门', () => { + const root = path.resolve(__dirname, '../../'); + const indexCode = fs.readFileSync(path.join(root, 'server/src/index.ts'), 'utf-8'); + const mysqlCode = fs.readFileSync(path.join(root, 'server/src/db/mysql.ts'), 'utf-8'); + const adminCode = fs.readFileSync( + path.join(root, 'admin/arco-design-pro-vite/src/views/safety/platform-config/index.vue'), + 'utf-8' + ); + + it('后端不再有硬编码 appId/appSecret 兜底', () => { + expect(indexCode).not.toContain("|| 'wx0c929f7e54024e67'"); + expect(indexCode).not.toContain("|| '63a412f3f73af8e73f6923943bf9683c'"); + expect(indexCode).toContain("appId: process.env.WX_APP_ID || ''"); + expect(indexCode).toContain("appSecret: process.env.WX_APP_SECRET || ''"); + }); + + it('缺失凭证时按接口 fail-closed 并给出明确提示', () => { + expect(indexCode).toContain('服务端未配置小程序 AppID/AppSecret,无法换取 openid'); + expect(indexCode).toContain('服务端未配置小程序 AppID(后台「微信支付商户配置」或环境变量 WX_APP_ID)'); + }); + + it('建表种子为空值,并清理历史遗留的占位凭证', () => { + expect(mysqlCode).toContain('appId: \'\',\n appSecret: \'\','); + // 自愈逻辑由"写入占位值"改为"清空占位值" + expect(mysqlCode).toContain("if (curVal.appId === 'wx0c929f7e54024e67'"); + expect(mysqlCode).toContain("if (curVal.appSecret === '63a412f3f73af8e73f6923943bf9683c')"); + expect(mysqlCode).toContain('curVal.appId = \'\''); + }); + + it('后台文案不再暴露具体 AppID', () => { + expect(adminCode).not.toContain('wx0c929f7e54024e67'); + }); +});