From fc4a6a7afcfc3c1c11d1e353f46d5b724f48a39e Mon Sep 17 00:00:00 2001 From: hectorzhao Date: Fri, 28 Aug 2026 12:00:55 +0800 Subject: [PATCH] fix: enforce strong hashes in maintenance tools --- tools/deploy/ensure-production-admin.mjs | 14 +++++++------- tools/quality/verify-code-quality.mjs | 12 ++++++++++++ tools/smoke/real-env-smoke.mjs | 10 ++++++---- 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/tools/deploy/ensure-production-admin.mjs b/tools/deploy/ensure-production-admin.mjs index 3f6a47d..649ada2 100644 --- a/tools/deploy/ensure-production-admin.mjs +++ b/tools/deploy/ensure-production-admin.mjs @@ -1,4 +1,4 @@ -import { createHash, randomBytes } from 'node:crypto'; +import { randomBytes } from 'node:crypto'; import { writeFileSync } from 'node:fs'; import { PrismaPg } from '../../api/node_modules/@prisma/adapter-pg/dist/index.js'; import { PrismaClient } from '../../api/node_modules/@prisma/client/index.js'; @@ -14,16 +14,16 @@ const email = process.env.PROD_ADMIN_EMAIL || 'admin@example.com'; const configuredPassword = process.env.PROD_ADMIN_PASSWORD; const credentialFile = process.env.PROD_ADMIN_CREDENTIAL_FILE; -function hashPassword(value) { - return createHash('sha256').update(value).digest('hex'); -} - async function main() { + // production-deploy.sh builds the API before invoking this helper, so every + // administrative password write shares the same versioned hasher as runtime APIs. + const { hashPassword } = await import('../../api/dist/auth/password-hasher.js'); const existingUser = await prisma.user.findFirst({ where: { username, deletedAt: null }, }); const createPassword = configuredPassword || randomBytes(18).toString('base64url'); const updatePassword = configuredPassword; + const passwordHash = await hashPassword(updatePassword || createPassword); const role = await prisma.role.upsert({ where: { code: 'platform_admin' }, update: { name: '平台管理员', scope: 'platform' }, @@ -32,7 +32,7 @@ async function main() { const userData = { email, displayName: '生产平台管理员', - ...(updatePassword ? { passwordHash: hashPassword(updatePassword) } : {}), + ...(updatePassword ? { passwordHash } : {}), status: 'active', failedLoginCount: 0, lockedUntil: null, @@ -49,7 +49,7 @@ async function main() { username, email, displayName: '生产平台管理员', - passwordHash: hashPassword(createPassword), + passwordHash, status: 'active', }, }); diff --git a/tools/quality/verify-code-quality.mjs b/tools/quality/verify-code-quality.mjs index 5b0b8ee..1f6764d 100644 --- a/tools/quality/verify-code-quality.mjs +++ b/tools/quality/verify-code-quality.mjs @@ -34,6 +34,18 @@ const authService = readFileSync(resolve(root, 'api/src/auth/auth.service.ts'), if (/passwordHash\s*===|===\s*[^\n;]*passwordHash/.test(authService)) { violations.push('api/src/auth/auth.service.ts: password hashes must not be compared directly'); } +const ensureAdmin = readFileSync(resolve(root, 'tools/deploy/ensure-production-admin.mjs'), 'utf8'); +if (/createHash\(['"]sha256['"]\)|function\s+hashPassword\s*\(/.test(ensureAdmin) + || !ensureAdmin.includes("api/dist/auth/password-hasher.js")) { + violations.push('tools/deploy/ensure-production-admin.mjs: administrative password writes must use the API password hasher'); +} +for (const relativePath of ['tools/deploy/ensure-production-admin.mjs', 'tools/smoke/real-env-smoke.mjs']) { + const content = readFileSync(resolve(root, relativePath), 'utf8'); + if (/function\s+hashPassword\s*\(|passwordHash:\s*(?:createHash|legacyHashPassword)/.test(content) + || !content.includes("api/dist/auth/password-hasher.js")) { + violations.push(`${relativePath}: user password writes must use the compiled API password hasher`); + } +} const packageJson = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8')); if (packageJson.dependencies?.['react-router-dom'] !== '7.18.2') { diff --git a/tools/smoke/real-env-smoke.mjs b/tools/smoke/real-env-smoke.mjs index dc4effd..511579d 100644 --- a/tools/smoke/real-env-smoke.mjs +++ b/tools/smoke/real-env-smoke.mjs @@ -14,11 +14,13 @@ const email = 'smoke-client@example.com'; const phone = '13800138099'; const password = 'SmokePass123!'; -function hashPassword(value) { +function hashApplicationSecret(value) { return createHash('sha256').update(value).digest('hex'); } async function ensureSmokeData() { + const { hashPassword } = await import('../../api/dist/auth/password-hasher.js'); + const userPasswordHash = await hashPassword(password); const tenant = await prisma.tenant.upsert({ where: { code: tenantCode }, update: { name: 'Smoke Test Enterprise', status: 'active' }, @@ -36,7 +38,7 @@ async function ensureSmokeData() { email, phone, displayName: 'Smoke Client Admin', - passwordHash: hashPassword(password), + passwordHash: userPasswordHash, status: 'active', failedLoginCount: 0, lockedUntil: null, @@ -50,7 +52,7 @@ async function ensureSmokeData() { email, phone, displayName: 'Smoke Client Admin', - passwordHash: hashPassword(password), + passwordHash: userPasswordHash, status: 'active', }, }); @@ -130,7 +132,7 @@ async function ensureSmokeData() { tenantId: tenant.id, name: 'Smoke SMS App', scene: 'smoke', - secretHash: hashPassword('smoke-secret'), + secretHash: hashApplicationSecret('smoke-secret'), dailyLimit: 100000, customerUnitPrice: 5, maxPhonesPerTask: 100000,