fix(platform): close remaining audit findings from security review
Harden preview/deploy flows, OTP generation, zip extraction, and multi-replica billing races; document full remediation status in AUDIT-STATUS.fa.md. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -13,6 +13,7 @@ import {
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { IsNull, LessThan, Repository } from 'typeorm';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import * as crypto from 'crypto';
|
||||
import { VerificationCode } from './entities/verification-code.entity';
|
||||
import { User } from './entities/user.entity';
|
||||
import { UsersService } from './users.service';
|
||||
@@ -185,7 +186,7 @@ export class VerificationService implements OnModuleInit, OnModuleDestroy {
|
||||
{ consumedAt: new Date() },
|
||||
);
|
||||
|
||||
const code = String(Math.floor(100000 + Math.random() * 900000)); // 6 digits
|
||||
const code = String(crypto.randomInt(100000, 1000000)); // 6 digits, CSPRNG
|
||||
const expiresAt = new Date(Date.now() + CODE_TTL_MS);
|
||||
const record = this.codeRepo.create({
|
||||
userId,
|
||||
@@ -208,30 +209,33 @@ export class VerificationService implements OnModuleInit, OnModuleDestroy {
|
||||
purpose: VerificationPurpose,
|
||||
code: string,
|
||||
): Promise<VerificationCode> {
|
||||
const record = await this.codeRepo.findOne({
|
||||
where: { userId, purpose, consumedAt: IsNull() },
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
return this.codeRepo.manager.transaction(async (em) => {
|
||||
const record = await em.findOne(VerificationCode, {
|
||||
where: { userId, purpose, consumedAt: IsNull() },
|
||||
order: { createdAt: 'DESC' },
|
||||
lock: { mode: 'pessimistic_write' },
|
||||
});
|
||||
|
||||
if (!record || record.expiresAt.getTime() < Date.now()) {
|
||||
throw new BadRequestException('No active code — request a new one');
|
||||
}
|
||||
if (record.attempts >= MAX_VERIFY_ATTEMPTS) {
|
||||
record.consumedAt = new Date();
|
||||
await em.save(record);
|
||||
throw new BadRequestException('Too many attempts — request a new code');
|
||||
}
|
||||
|
||||
const ok = await bcrypt.compare(code, record.codeHash);
|
||||
if (!ok) {
|
||||
record.attempts += 1;
|
||||
await em.save(record);
|
||||
throw new BadRequestException('Invalid code');
|
||||
}
|
||||
|
||||
if (!record || record.expiresAt.getTime() < Date.now()) {
|
||||
throw new BadRequestException('No active code — request a new one');
|
||||
}
|
||||
if (record.attempts >= MAX_VERIFY_ATTEMPTS) {
|
||||
record.consumedAt = new Date();
|
||||
await this.codeRepo.save(record);
|
||||
throw new BadRequestException('Too many attempts — request a new code');
|
||||
}
|
||||
|
||||
const ok = await bcrypt.compare(code, record.codeHash);
|
||||
if (!ok) {
|
||||
record.attempts += 1;
|
||||
await this.codeRepo.save(record);
|
||||
throw new BadRequestException('Invalid code');
|
||||
}
|
||||
|
||||
record.consumedAt = new Date();
|
||||
await this.codeRepo.save(record);
|
||||
return record;
|
||||
await em.save(record);
|
||||
return record;
|
||||
});
|
||||
}
|
||||
|
||||
/** Best-effort cleanup of long-expired codes (called opportunistically). */
|
||||
|
||||
Reference in New Issue
Block a user