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
+28 -16
View File
@@ -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