fix(security): harden auth, upload, and API configuration

- Lock public registration to first-user bootstrap (403 afterwards)
- Require JwtAuthGuard on upload + whitelist png/jpg/webp/gif (SVG/XSS blocked)
- Add global throttling (login/register 5/min, upload 10/min)
- Add helmet security headers; serve uploads with nosniff
- Replace permissive CORS (origin:true+credentials) with CORS_ORIGINS whitelist
- Disable Swagger outside development; sanitize 500 error responses
- Enforce 32+ char JWT_SECRET; make token expiry configurable (TOKEN_EXPIRES_IN)
- Re-check user in DB on every JWT validation (revocation on user delete)
- Dummy bcrypt compare to prevent login user-enumeration via timing
- Map malformed BigInt inputs to 400 instead of 500
- Widen .gitignore to .env* and add apps/api/.env.example
- Disable Nuxt devtools and sourcemaps
This commit is contained in:
yeuimu
2026-08-22 11:55:13 +08:00
parent 9ed569f5bc
commit 9c1106586a
14 changed files with 240 additions and 112 deletions
+18 -4
View File
@@ -2,6 +2,7 @@ import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';
import { PrismaService } from '../../prisma/prisma.service';
/**
* Shape of the JWT we issue.
@@ -15,11 +16,17 @@ export interface JwtPayload {
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(config: ConfigService) {
constructor(
config: ConfigService,
private readonly prisma: PrismaService,
) {
const secret = config.get<string>('JWT_SECRET');
if (!secret) {
throw new Error('JWT_SECRET is not configured');
}
if (secret.length < 32) {
throw new Error('JWT_SECRET must be at least 32 characters');
}
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
@@ -29,12 +36,19 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
/**
* Runs on every authenticated request. The returned object becomes
* `request.user` for downstream controllers.
* `request.user` for downstream controllers. The user is re-checked in
* the database so tokens of deleted users stop working immediately.
*/
validate(payload: JwtPayload): { id: bigint; username: string } {
async validate(payload: JwtPayload): Promise<{ id: bigint; username: string }> {
if (!payload?.sub || !payload.username) {
throw new UnauthorizedException('Invalid token payload');
}
return { id: BigInt(payload.sub), username: payload.username };
const user = await this.prisma.user
.findUnique({ where: { id: BigInt(payload.sub) } })
.catch(() => null);
if (!user || user.username !== payload.username) {
throw new UnauthorizedException('Invalid token');
}
return { id: user.id, username: user.username };
}
}