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:
keyhan
2026-07-03 12:30:22 +03:30
parent 6d9cd89cc5
commit 8163665c86
8 changed files with 294 additions and 87 deletions
+27 -23
View File
@@ -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). */