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 验证缺失即失败
这个提交包含在:
@@ -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' ? '钱包余额充值' : '同城陪伴担保订单');
|
||||
|
||||
|
||||
在新工单中引用
屏蔽一个用户