security: 后台默认口令生产 fail-closed(修复 admin/admin 可登录的高危漏洞)

- 生产环境未配置 ADMIN_PASSWORD(或仍为 admin/admin123)时,后台登录直接 503 拒绝
- 启动自检新增两条告警:未配有效 ADMIN_PASSWORD、非 production 身份校验放宽
- 部署文档补充 ADMIN_PASSWORD/ADMIN_USERNAME 必配说明
- 新增回归护栏 admin-credential-hardening.spec.ts(88 套件/642 用例全绿);冒烟 5/5
这个提交包含在:
xcbyy
2026-10-19 13:36:36 +08:00
父节点 b88442af28
当前提交 699d2bd951
共有 3 个文件被更改,包括 54 次插入 和 1 次删除

查看文件

@@ -14,6 +14,10 @@ PORT=3000
ADMIN_TOKEN_SECRET=<强随机长串>
USER_TOKEN_SECRET=<另一个强随机长串>
# 后台登录口令(生产必配;未配置或仍为 admin/admin123 时后台登录被禁用)
ADMIN_USERNAME=<后台账号,默认 admin>
ADMIN_PASSWORD=<强口令;多个可用 | 分隔>
# 数据库
MYSQL_HOST=127.0.0.1
MYSQL_PORT=3306

查看文件

@@ -180,7 +180,10 @@ const ADMIN_TOKEN_SECRET = (() => {
return DEFAULT_ADMIN_TOKEN_SECRET;
})();
const ADMIN_USERNAME = process.env.ADMIN_USERNAME || 'admin';
const ADMIN_PASSWORDS = (process.env.ADMIN_PASSWORD || 'admin|admin123').split('|').filter(Boolean);
const ADMIN_PASSWORD_SET = String(process.env.ADMIN_PASSWORD || '').trim();
const ADMIN_PASSWORDS = (ADMIN_PASSWORD_SET || 'admin|admin123').split('|').filter(Boolean);
// 生产环境安全闸门:未配置 ADMIN_PASSWORD(或仍使用内置默认口令)时,禁止后台登录,避免默认口令被爆破
const ADMIN_PASSWORD_CONFIGURED = Boolean(ADMIN_PASSWORD_SET) && !/^(admin|admin123)$/i.test(ADMIN_PASSWORD_SET);
function signAdminToken(): string {
const payload = Buffer.from(JSON.stringify({ role: 'admin', ts: Date.now() })).toString('base64url');
@@ -376,6 +379,14 @@ app.post('/api/user/login', (req: Request, res: Response) => {
if (isLoginLocked(loginKey)) {
return res.status(429).json({ code: 50000, message: '登录失败次数过多,请 10 分钟后再试' });
}
// 生产环境 fail-closed:未配置 ADMIN_PASSWORD(或仍为内置默认口令)时,禁止后台登录
if (IS_PRODUCTION && !ADMIN_PASSWORD_CONFIGURED) {
logServerError('adminLogin', new Error('生产环境未配置 ADMIN_PASSWORD(或仍为默认口令),已禁用后台登录'));
return res.status(503).json({
code: 50000,
message: '生产环境未配置后台登录口令:请设置环境变量 ADMIN_PASSWORD(不得为 admin/admin123)后重启服务',
});
}
// 管理凭证默认 admin/admin,可通过环境变量 ADMIN_USERNAME / ADMIN_PASSWORD 覆盖
const passwordOk = ADMIN_PASSWORDS.some((p) => safeStringEqual(String(password), p));
if (safeStringEqual(String(username), ADMIN_USERNAME) && passwordOk) {
@@ -4497,6 +4508,16 @@ async function logStartupConfigSelfCheck(): Promise<void> {
console.log(`[Peiwan Config] ADMIN_TOKEN_SECRET=${adminSecretSet ? '已设置' : '开发默认值'} USER_TOKEN_SECRET=${userSecretSet ? '已设置' : '按 ADMIN 派生'}`);
}
// 后台登录口令闸门:生产未配置(或为默认口令)时后台登录被禁用
if (isProd && !ADMIN_PASSWORD_CONFIGURED) {
console.warn('[Peiwan Config][WARN] 未设置有效的 ADMIN_PASSWORD(不得为 admin/admin123):生产环境后台登录已被禁用,请尽快设置后重启。');
}
// 非生产环境身份校验放宽(允许客户端声明身份),禁止用于公网部署
if (!isProd) {
console.warn('[Peiwan Config][WARN] 当前非 production:用户身份校验已放宽(未登录请求将采用客户端声明的 userId),仅限本地联调,切勿用于公网部署。');
}
const cfg: any = await resolveWechatPayRuntimeConfig();
const missing: string[] = [];
if (!cfg.appId) missing.push('appId');

查看文件

@@ -0,0 +1,28 @@
// 已读取AGENTS.md
import * as fs from 'fs';
import * as path from 'path';
/**
* 第七十二批回归闸门:后台默认口令生产 fail-closed。
* 背景:ADMIN_PASSWORD 未配置时默认 admin/admin123,公网部署等于开放后台。
*/
describe('第七十二批后台凭证闸门', () => {
const root = path.resolve(__dirname, '../../');
const indexCode = fs.readFileSync(path.join(root, 'server/src/index.ts'), 'utf-8');
const doc = fs.readFileSync(path.join(root, 'docs/部署验收与1分钱支付闭环.md'), 'utf-8');
it('生产未配置 ADMIN_PASSWORD 时禁用后台登录(fail-closed)', () => {
expect(indexCode).toContain('ADMIN_PASSWORD_CONFIGURED');
expect(indexCode).toContain('if (IS_PRODUCTION && !ADMIN_PASSWORD_CONFIGURED)');
expect(indexCode).toContain('生产环境未配置后台登录口令');
});
it('启动自检提示默认口令与非生产放宽风险', () => {
expect(indexCode).toContain('未设置有效的 ADMIN_PASSWORD');
expect(indexCode).toContain('用户身份校验已放宽');
});
it('部署文档包含 ADMIN_PASSWORD', () => {
expect(doc).toContain('ADMIN_PASSWORD');
});
});