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
这个提交包含在:
@@ -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');
|
||||
|
||||
在新工单中引用
屏蔽一个用户