-
Notifications
You must be signed in to change notification settings - Fork 74
Enhanced Security Module #204
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -21,6 +21,8 @@ import { SupabaseModule } from './modules/supabase/supabase.module'; | |||||||||||||||||||||||||||
| import { EscrowModule } from './modules/escrow/escrow.module'; | ||||||||||||||||||||||||||||
| import { AppCacheModule } from './cache/cache.module'; | ||||||||||||||||||||||||||||
| import { StoresModule } from './modules/stores/stores.module'; | ||||||||||||||||||||||||||||
| import { SecurityModule } from './security/security.module'; | ||||||||||||||||||||||||||||
| import { ThrottlerGuard } from '@nestjs/throttler'; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // Entities | ||||||||||||||||||||||||||||
| import { User } from './modules/users/entities/user.entity'; | ||||||||||||||||||||||||||||
|
|
@@ -103,9 +105,16 @@ import { EscrowsModule } from './modules/escrows/escrows.module'; | |||||||||||||||||||||||||||
| OffersModule, | ||||||||||||||||||||||||||||
| EscrowModule, | ||||||||||||||||||||||||||||
| SupabaseModule, | ||||||||||||||||||||||||||||
| EscrowModule, | ||||||||||||||||||||||||||||
| EscrowModule, | ||||||||||||||||||||||||||||
| StoresModule, | ||||||||||||||||||||||||||||
| EscrowsModule, | ||||||||||||||||||||||||||||
| SecurityModule, | ||||||||||||||||||||||||||||
|
Comment on lines
106
to
+111
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove duplicate EscrowModule import.
Apply this diff to remove the duplicate: OffersModule,
EscrowModule,
SupabaseModule,
- EscrowModule,
StoresModule,
EscrowsModule,
SecurityModule,📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||
| providers: [ | ||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||
| provide: 'APP_GUARD', | ||||||||||||||||||||||||||||
| useClass: ThrottlerGuard, | ||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||
| export class AppModule { } | ||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -6,24 +6,72 @@ import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { BuyerRequestSchedulerService } from './modules/buyer-requests/services/buyer-request-scheduler.service'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { ResponseInterceptor } from './common/interceptors/response.interceptor'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { HttpExceptionFilter } from './common/filters/http-exception.filter'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { ConfigService } from '@nestjs/config'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import helmet from 'helmet'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import * as bodyParser from 'body-parser'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { ThrottlerGuard } from '@nestjs/throttler'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async function bootstrap(): Promise<void> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const app = await NestFactory.create(AppModule); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const configService = app.get(ConfigService); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Middleware to track request start time for latency | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| app.use((req, res, next) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| req._startTime = Date.now(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| next(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Enable CORS | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| app.enableCors(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Enable CORS with allowlist from config | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const allowedOrigins = configService.get<string[]>('security.allowedOrigins', ['http://localhost:3000']); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| app.enableCors({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| origin: (origin, callback) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!origin || allowedOrigins.includes(origin)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| callback(null, true); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| callback(new Error('Not allowed by CORS')); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| credentials: true, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+25
to
+35
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Trim allowlist entries before CORS check. When ALLOWED_ORIGINS comes from env vars like - const allowedOrigins = configService.get<string[]>('security.allowedOrigins', ['http://localhost:3000']);
+ const rawAllowedOrigins = configService.get<string[]>('security.allowedOrigins', ['http://localhost:3000']);
+ const allowedOrigins = rawAllowedOrigins
+ .map((origin) => origin.trim())
+ .filter((origin) => origin.length > 0);🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Global validation pipe | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| app.useGlobalPipes( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Set body size limits | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| app.use(bodyParser.json({ limit: configService.get<string>('security.bodyLimit', '1mb') })); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| app.use(bodyParser.urlencoded({ extended: true, limit: configService.get<string>('security.bodyLimit', '1mb') })); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Trust proxy if enabled | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (configService.get<boolean>('security.trustProxy', false)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const expressApp = app.getHttpAdapter().getInstance(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (expressApp && typeof expressApp.set === 'function') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expressApp.set('trust proxy', 1); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Helmet security headers | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| app.use( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| helmet({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| contentSecurityPolicy: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| useDefaults: true, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| directives: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| defaultSrc: [configService.get<string>('security.cspDefaultSrc', "'self'")], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| crossOriginEmbedderPolicy: true, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| crossOriginResourcePolicy: { policy: 'same-origin' }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| frameguard: { action: 'deny' }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| hsts: process.env.NODE_ENV === 'production' ? undefined : false, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| noSniff: true, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| xssFilter: true, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+57
to
+69
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major Use config-derived NODE_ENV instead of Directly touching const app = await NestFactory.create(AppModule);
- const configService = app.get(ConfigService);
+ const configService = app.get(ConfigService);
+ const nodeEnv = configService.get<string>('NODE_ENV', 'development');
@@
- hsts: process.env.NODE_ENV === 'production' ? undefined : false,
+ hsts: nodeEnv === 'production' ? undefined : false,📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Global rate limiting guard | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| app.useGlobalGuards(new ThrottlerGuard()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+72
to
+74
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instantiate
- app.useGlobalGuards(new ThrottlerGuard());
+ app.useGlobalGuards(app.get(ThrottlerGuard));🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Global response interceptor | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| app.useGlobalInterceptors(new ResponseInterceptor()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,10 @@ | ||||||||||||||||||||||
| import { registerAs } from '@nestjs/config'; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| export default registerAs('security', () => ({ | ||||||||||||||||||||||
| allowedOrigins: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'], | ||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Normalize origin list when splitting env var. Comma-delimited env values often carry spaces or blank entries, which later fail the strict -export default registerAs('security', () => ({
- allowedOrigins: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'],
+export default registerAs('security', () => ({
+ allowedOrigins: process.env.ALLOWED_ORIGINS
+ ? process.env.ALLOWED_ORIGINS
+ .split(',')
+ .map((origin) => origin.trim())
+ .filter((origin) => origin.length > 0)
+ : ['http://localhost:3000'],📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||
| rateLimitPoints: parseInt(process.env.RATE_LIMIT_POINTS || '100', 10), | ||||||||||||||||||||||
| rateLimitDuration: parseInt(process.env.RATE_LIMIT_DURATION || '60', 10), | ||||||||||||||||||||||
|
Comment on lines
+5
to
+6
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Guard against NaN when parsing rate-limit settings. If RATE_LIMIT_POINTS or RATE_LIMIT_DURATION contain non-numeric input, - rateLimitPoints: parseInt(process.env.RATE_LIMIT_POINTS || '100', 10),
- rateLimitDuration: parseInt(process.env.RATE_LIMIT_DURATION || '60', 10),
+ rateLimitPoints: (() => {
+ const parsed = Number.parseInt(process.env.RATE_LIMIT_POINTS ?? '', 10);
+ return Number.isNaN(parsed) ? 100 : parsed;
+ })(),
+ rateLimitDuration: (() => {
+ const parsed = Number.parseInt(process.env.RATE_LIMIT_DURATION ?? '', 10);
+ return Number.isNaN(parsed) ? 60 : parsed;
+ })(),📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||
| trustProxy: process.env.TRUST_PROXY === 'true', | ||||||||||||||||||||||
| bodyLimit: process.env.BODY_LIMIT || '1mb', | ||||||||||||||||||||||
| cspDefaultSrc: process.env.CSP_DEFAULT_SRC || "'self'", | ||||||||||||||||||||||
| })); | ||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| import { Module } from '@nestjs/common'; | ||
| import { ConfigModule } from '@nestjs/config'; | ||
| import securityConfig from './security.config'; | ||
| import { ThrottlerModule } from '@nestjs/throttler'; | ||
|
|
||
| @Module({ | ||
| imports: [ | ||
| ConfigModule.forRoot({ | ||
| isGlobal: true, | ||
| load: [securityConfig], | ||
| }), | ||
|
Comment on lines
+8
to
+11
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove duplicate ConfigModule registration. ConfigModule is already registered globally in Apply this diff to remove the duplicate registration: @Module({
imports: [
- ConfigModule.forRoot({
- isGlobal: true,
- load: [securityConfig],
- }),
ThrottlerModule.forRootAsync({
- useFactory: (config) => ({
+ imports: [ConfigModule],
+ inject: [ConfigService],
+ useFactory: (config: ConfigService) => ({
ttl: config.get('security.rateLimitDuration'),
limit: config.get('security.rateLimitPoints'),
}),
- inject: [ConfigModule],
}),
],
})Ensure #!/bin/bash
# Description: Verify security config is loaded in app.module.ts ConfigModule
echo "=== Checking app.module.ts ConfigModule registration ==="
rg -n -A5 "ConfigModule\.forRoot" src/app.module.ts
echo -e "\n=== Checking if securityConfig is imported in app.module.ts ==="
rg -n "securityConfig" src/app.module.ts || echo "securityConfig not found in app.module.ts - needs to be added to ConfigModule.forRoot load array"🤖 Prompt for AI Agents |
||
| ThrottlerModule.forRootAsync({ | ||
| useFactory: (config) => ({ | ||
| ttl: config.get('security.rateLimitDuration'), | ||
| limit: config.get('security.rateLimitPoints'), | ||
| }), | ||
| inject: [ConfigModule], | ||
| }), | ||
| ], | ||
| }) | ||
| export class SecurityModule {} | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove quotes from CSP_DEFAULT_SRC environment variable value.
The quotes around
'self'will become part of the environment variable value, resulting in"'self'"(including the quotes) being read by the application. When this value is passed to Helmet's CSP configuration, it will be double-quoted and invalid.Apply this diff to fix the environment variable:
The security configuration code (src/security/security.config.ts) should add the required quotes when constructing the CSP directive for Helmet, as CSP requires quoted keywords like
'self'.Verify that security.config.ts properly quotes the value when passing it to Helmet:
🧰 Tools
🪛 dotenv-linter (3.3.0)
[warning] 43-43: [QuoteCharacter] The value has quote characters (', ")
(QuoteCharacter)
[warning] 43-43: [UnorderedKey] The CSP_DEFAULT_SRC key should go before the RATE_LIMIT_DURATION key
(UnorderedKey)
🤖 Prompt for AI Agents