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:
+28
-16
@@ -2,6 +2,7 @@ import { NestFactory } from '@nestjs/core';
|
||||
import { NestExpressApplication } from '@nestjs/platform-express';
|
||||
import { ValidationPipe } from '@nestjs/common';
|
||||
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
||||
import helmet from 'helmet';
|
||||
import { json } from 'express';
|
||||
import { join } from 'path';
|
||||
import { AppModule } from './app.module';
|
||||
@@ -28,15 +29,24 @@ async function bootstrap() {
|
||||
}),
|
||||
);
|
||||
|
||||
// CORS
|
||||
app.enableCors({
|
||||
origin: true,
|
||||
credentials: true,
|
||||
});
|
||||
// Security headers (X-Content-Type-Options, X-Frame-Options, CSP, HSTS, ...)
|
||||
app.use(helmet());
|
||||
|
||||
// Serve uploaded files
|
||||
// CORS: only origins listed in CORS_ORIGINS (comma-separated) are allowed.
|
||||
// Authentication uses Bearer headers, so credentialed CORS is not needed.
|
||||
const corsOrigins = (process.env.CORS_ORIGINS ?? '')
|
||||
.split(',')
|
||||
.map((o) => o.trim())
|
||||
.filter(Boolean);
|
||||
app.enableCors(corsOrigins.length > 0 ? { origin: corsOrigins } : undefined);
|
||||
|
||||
// Serve uploaded files. nosniff prevents browsers from sniffing a
|
||||
// non-image content type out of an uploaded file.
|
||||
app.useStaticAssets(join(process.cwd(), 'uploads'), {
|
||||
prefix: '/uploads/',
|
||||
setHeaders: (res) => {
|
||||
res.setHeader('X-Content-Type-Options', 'nosniff');
|
||||
},
|
||||
});
|
||||
app.useStaticAssets(join(process.cwd(), 'public'), {
|
||||
prefix: '/assets/',
|
||||
@@ -55,21 +65,23 @@ async function bootstrap() {
|
||||
app.useGlobalFilters(new HttpExceptionFilter());
|
||||
app.useGlobalInterceptors(new TransformInterceptor());
|
||||
|
||||
// Swagger
|
||||
const config = new DocumentBuilder()
|
||||
.setTitle('InkReach Product Center API')
|
||||
.setDescription('Backend API for InkReach Product Center')
|
||||
.setVersion('1.0')
|
||||
.addBearerAuth()
|
||||
.build();
|
||||
// Swagger is only exposed outside production to avoid leaking the
|
||||
// full admin API surface.
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
const config = new DocumentBuilder()
|
||||
.setTitle('InkReach Product Center API')
|
||||
.setDescription('Backend API for InkReach Product Center')
|
||||
.setVersion('1.0')
|
||||
.addBearerAuth()
|
||||
.build();
|
||||
|
||||
const document = SwaggerModule.createDocument(app, config);
|
||||
SwaggerModule.setup('api/docs', app, document);
|
||||
const document = SwaggerModule.createDocument(app, config);
|
||||
SwaggerModule.setup('api/docs', app, document);
|
||||
}
|
||||
|
||||
const port = process.env.PORT ?? 3001;
|
||||
await app.listen(port, '0.0.0.0');
|
||||
console.log(`🚀 Application is running on: http://0.0.0.0:${port}`);
|
||||
console.log(`📚 Swagger documentation: http://0.0.0.0:${port}/api/docs`);
|
||||
}
|
||||
|
||||
// Make JSON.stringify aware of BigInt so outgoing responses containing
|
||||
|
||||
Reference in New Issue
Block a user