From ae436c01ca47438b6903456d0be1b0244cc556d0 Mon Sep 17 00:00:00 2001 From: qubiaobiao <3294694717@qq.com> Date: Wed, 14 Oct 2026 10:47:51 +0800 Subject: [PATCH] =?UTF-8?q?fix(auth):=20seeder=20002=20=E6=94=B9=E7=A7=8D?= =?UTF-8?q?=20admins=20+=20=E9=BB=98=E8=AE=A4=E5=8F=A3=E4=BB=A4=E5=AE=88?= =?UTF-8?q?=E5=8D=AB=E8=A1=A5=20'admin'=EF=BC=88=E5=80=99=E9=80=89?= =?UTF-8?q?=E2=91=B3/=E3=89=91=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 候选⑳:002-seed-admin.js 原 bulkInsert 到 accounts 表(row 含 role/name_cn/salt 等列),但登录链路读 admins 表(service/auth.js:101 findAdmin() → model/admin.js:49 tableName='admins')⇒ 原写法属死写入,种下的行永不被登录读取。改为写入 admins 并按 其真实列契约(id/username/password/created_at/updated_at)重写 row;down() 同步改 bulkDelete('admins', {username:'admin'})。前置已全仓 grep 确认 accounts.role='admin' 零消费点(后端 4 处 Account 用法均按 id_number/uid 定位;role==='admin' 5 处判定全 作用于 JWT payload / 前端本地态)。 候选㉑:DEFAULT_ADMIN_PASSWORDS 原为 ['admin123','admin123456'],不含 'admin',而 schema.sql 建库路径的初始口令恰为 admin ⇒ 生产未改密时该口令可绕过 4005 守卫 (verifyPassword 兼容 SHA256 ⇒ 可直接登录)。已补入 'admin'。结构性缺口(口令字面 判据 ⇒ 未枚举的默认口令均绕过)未消除,登记为 BACKLOG 候选㉒。 同源引用同步(就地更正、零删除):schema.sql:213-218 注释、docs/modules.md:581 注释(含行号引用 378→387)。 验证:node --check 通过;R111 判别力自证 22/22 PASS + 4 项变异全红; verify_no_secrets HIGH 0 / CAND 2(与基线一致 ⇒ 零新增);verify_tautology_guard PASS;verify_constant_shape_guard PASS。 --- app/controller/auth.js | 13 ++++++++-- database/seeders/002-seed-admin.js | 41 +++++++++++++----------------- 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/app/controller/auth.js b/app/controller/auth.js index 78768da..11947b4 100644 --- a/app/controller/auth.js +++ b/app/controller/auth.js @@ -372,10 +372,19 @@ class AuthController extends Controller { return; } - // P2-3 默认口令守卫:schema.sql 内置默认管理员(admin/admin123), + // P2-3 默认口令守卫:内置默认管理员(见 database/schema.sql 与 database/seeders/002-seed-admin.js), // 生产环境禁止使用默认口令登录,防止默认账号直接上线被接管; // 非生产环境放行但记录告警,并在响应中提示尽快改密。 - const DEFAULT_ADMIN_PASSWORDS = ['admin123', 'admin123456']; + // + // ⚠️ 【R111 更正 · 实测】原注释写「schema.sql 内置默认管理员(admin/admin123)」,两处失实: + // ① 归属错 —— `admin123` 是 **seeder 002** 的 bcrypt 值;`schema.sql:214` 的字面哈希实测 = SHA256(`admin`)。 + // ② 列表漏项 —— 原列表 `['admin123','admin123456']` **不含 `admin`**,而 `schema.sql` 建库路径的初始 + // 口令恰为 `admin` ⇒ 生产若未改密,用 `admin/admin` 登录**不会**触发 4005(`verifyPassword` 兼容 + // SHA256 ⇒ 可直接登录成功)⇒ 恰是本守卫要防的场景却不设防。已补入 `'admin'`。 + // 详见 `.workbuddy/memory/BACKLOG.md` 候选⑱/㉑。 + // ⚠️ 结构性缺口**未消除**:本守卫是「口令字面」判据 ⇒ 任何未枚举的默认口令均绕过; + // 彻底治法应改为「哈希值判据」(DB 值 ∈ 已知默认哈希集)。见候选㉑。 + const DEFAULT_ADMIN_PASSWORDS = ['admin', 'admin123', 'admin123456']; const usingDefaultPassword = DEFAULT_ADMIN_PASSWORDS.includes(password); const isProd = this.app.config.isProduction; if (usingDefaultPassword && isProd) { diff --git a/database/seeders/002-seed-admin.js b/database/seeders/002-seed-admin.js index c40ed19..7f28494 100644 --- a/database/seeders/002-seed-admin.js +++ b/database/seeders/002-seed-admin.js @@ -4,43 +4,38 @@ // 口令:下方内置 bcrypt 哈希仅为**开发/演示**默认值(对应公开弱口令,仅供本机初始化)。 // 正式初始化(生产等)时应通过环境变量 ADMIN_INIT_PASSWORD 注入真实口令 —— // 本文件⛔不落盘任何明文口令(铁律 25)。 +// +// ⚠️ 【R111 更正 · 实测】本 seeder 原先 `bulkInsert` 到 **`accounts`** 表(row 含 +// role/name_cn/salt/id_number 等列);但登录链路读的是 **`admins`** 表 +// (`service/auth.js:101 findAdmin()` → `model/admin.js:49 tableName='admins'`) +// ⇒ 原写法属**死写入**(种下的那行永远不会被登录读取)。 +// 已改为写入 `admins`,并按该表真实列契约(`id/username/password/created_at/updated_at`, +// 见 `database/migrations/002-create-admins.js:17-46` 与 `database/schema.sql:20-29`)重写 row。 +// 详见 `.workbuddy/memory/BACKLOG.md` 候选⑳。 module.exports = { up: async (queryInterface, Sequelize) => { const now = new Date(); + // `admins` 表列契约:id(自增主键,勿传)/ username / password / created_at / updated_at。 + // ⛔ 该表**无** salt / role / name_cn / id_number / activated / score 等列。 const row = { - role: 'admin', - name_cn: '系统管理员', - name_en: 'Administrator', - gender: null, - phone: null, - email: 'admin@competition.com', - id_number: '000000000000000000', + username: 'admin', + // bcrypt(cost=10) 哈希 = 开发默认弱口令(公开值,仅供本机初始化;生产须经 ADMIN_INIT_PASSWORD 覆盖)。 + // 60 字符 < `admins.password VARCHAR(100)` ⇒ 列宽容得下。 password: '$2b$10$Nc6PCJItIwuOzpM22dTiAezB.jZMI8mzkCbg3oRZkAWx.QaI4co9.', - salt: '$2b$10$Nc6PCJItIwuOzpM22dTiAe', - position: '系统管理员', - major: null, - country: '中国', - region: null, - organization_cn: '赛事组委会', - organization_en: 'Competition Committee', - activated: 1, - score: null, created_at: now, updated_at: now, }; - // 若提供 ADMIN_INIT_PASSWORD,则以之为准重算 bcrypt 哈希(salt 取哈希前 29 字符的 bcrypt 盐前缀)。 + // 若提供 ADMIN_INIT_PASSWORD,则以之为准重算 bcrypt 哈希(明文只经过程环境,⛔ 不落盘)。 const initPwd = process.env.ADMIN_INIT_PASSWORD; if (initPwd) { - const hash = require('bcrypt').hashSync(String(initPwd), 10); - row.password = hash; - row.salt = hash.slice(0, 29); + row.password = require('bcrypt').hashSync(String(initPwd), 10); } - await queryInterface.bulkInsert('accounts', [row], {}); + await queryInterface.bulkInsert('admins', [row], {}); }, down: async (queryInterface, Sequelize) => { - await queryInterface.bulkDelete('accounts', { - id_number: '000000000000000000', + await queryInterface.bulkDelete('admins', { + username: 'admin', }, {}); }, };