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:
@@ -1,33 +1,45 @@
|
||||
import {
|
||||
Controller,
|
||||
Post,
|
||||
UseGuards,
|
||||
UseInterceptors,
|
||||
UploadedFile,
|
||||
BadRequestException,
|
||||
} from '@nestjs/common';
|
||||
import { Throttle } from '@nestjs/throttler';
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { diskStorage } from 'multer';
|
||||
import { extname, join } from 'path';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
|
||||
const UPLOAD_DIR = join(process.cwd(), 'uploads');
|
||||
|
||||
// Explicit safe-image whitelist. SVG is deliberately excluded: it can
|
||||
// carry scripts and is served from the same origin (stored XSS).
|
||||
const ALLOWED_EXTENSIONS = /\.(png|jpe?g|webp|gif)$/i;
|
||||
const ALLOWED_MIMETYPES = /^image\/(png|jpe?g|webp|gif)$/i;
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Controller('upload')
|
||||
export class UploadController {
|
||||
@Post('image')
|
||||
@Throttle({ default: { limit: 10, ttl: 60_000 } })
|
||||
@UseInterceptors(
|
||||
FileInterceptor('file', {
|
||||
storage: diskStorage({
|
||||
destination: UPLOAD_DIR,
|
||||
filename: (_req, file, cb) => {
|
||||
const ext = extname(file.originalname) || '.png';
|
||||
const ext = ALLOWED_EXTENSIONS.test(extname(file.originalname))
|
||||
? extname(file.originalname).toLowerCase()
|
||||
: '.png';
|
||||
cb(null, `${randomUUID()}${ext}`);
|
||||
},
|
||||
}),
|
||||
limits: { fileSize: 5 * 1024 * 1024 },
|
||||
fileFilter: (_req, file, cb) => {
|
||||
if (!file.mimetype.startsWith('image/')) {
|
||||
return cb(new BadRequestException('仅支持图片文件'), false);
|
||||
if (!ALLOWED_EXTENSIONS.test(file.originalname) || !ALLOWED_MIMETYPES.test(file.mimetype)) {
|
||||
return cb(new BadRequestException('仅支持 png/jpg/webp/gif 图片'), false);
|
||||
}
|
||||
cb(null, true);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user