security: 移除硬编码微信小程序 appId/appSecret,改为只读配置+缺失即失败
- 后端不再兜底内置 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 验证缺失即失败
这个提交包含在:
@@ -150,7 +150,7 @@
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
label="微信小程序 AppID (appId)"
|
||||
help="微信公众平台分配的小程序 AppID,如 wx0c929f7e54024e67"
|
||||
help="微信公众平台分配的小程序 AppID,例如 wx1234567890abcdef"
|
||||
>
|
||||
<a-input v-model="wxPayForm.appId" placeholder="请输入 wx 开头的小程序 AppID" allow-clear />
|
||||
</a-form-item>
|
||||
|
||||
@@ -796,7 +796,7 @@
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="微信小程序 AppID (appId)">
|
||||
<a-input v-model="wxPayForm.valueJson.appId" placeholder="例如:wx0c929f7e54024e67" allow-clear />
|
||||
<a-input v-model="wxPayForm.valueJson.appId" placeholder="例如:wx1234567890abcdef" allow-clear />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
@@ -974,7 +974,7 @@
|
||||
<a-alert type="info" style="margin-bottom: 20px">
|
||||
在此维护微信小程序的地图定位服务参数(腾讯位置服务 Tencent LBS Key)。用于同城组局发布时的地图选点(chooseLocation)、经纬度逆地址解析及附近搭子/活动测距。
|
||||
<div style="margin-top: 6px; font-size: 12px; color: var(--color-text-3);">
|
||||
配置指引:登录腾讯位置服务控制台 (lbs.qq.com) -> 创建应用并添加 Key -> 勾选 WebServiceAPI 与微信小程序授权(填入小程序 AppID: wx0c929f7e54024e67)。
|
||||
配置指引:登录腾讯位置服务控制台 (lbs.qq.com) -> 创建应用并添加 Key -> 勾选 WebServiceAPI 与微信小程序授权(填入你的小程序 AppID)。
|
||||
</div>
|
||||
</a-alert>
|
||||
|
||||
@@ -1002,7 +1002,7 @@
|
||||
allow-clear
|
||||
/>
|
||||
<template #extra>
|
||||
需在腾讯位置服务后台开通 WebServiceAPI,并授权绑定小程序 AppID: <strong>wx0c929f7e54024e67</strong>。
|
||||
需在腾讯位置服务后台开通 WebServiceAPI,并授权绑定小程序 AppID。
|
||||
</template>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
@@ -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 与微信小程序)'
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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=...
|
||||
|
||||
@@ -1034,8 +1034,8 @@ async function seedInitialDataIfEmpty(conn: any): Promise<void> {
|
||||
|
||||
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<void> {
|
||||
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<void> {
|
||||
}
|
||||
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) {
|
||||
|
||||
@@ -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' ? '钱包余额充值' : '同城陪伴担保订单');
|
||||
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
在新工单中引用
屏蔽一个用户