cyberaudit-skill 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +77 -0
  2. package/dist/cli.d.ts +2 -0
  3. package/dist/cli.js +241 -0
  4. package/dist/mcp-server.d.ts +2 -0
  5. package/dist/mcp-server.js +126 -0
  6. package/package.json +58 -0
  7. package/skills/cyberaudit/AGENT-BOOT.md +122 -0
  8. package/skills/cyberaudit/COMMANDS.md +1125 -0
  9. package/skills/cyberaudit/INSTALL.md +311 -0
  10. package/skills/cyberaudit/MASTER.md +194 -0
  11. package/skills/cyberaudit/README.md +154 -0
  12. package/skills/cyberaudit/USAGE-GUIDE.md +107 -0
  13. package/skills/cyberaudit/mobile/MOBILE-CHECKLIST.md +245 -0
  14. package/skills/cyberaudit/mobile/MOBILE-PHILOSOPHY.md +352 -0
  15. package/skills/cyberaudit/mobile/MOBILE-REMEDIATION-LIBRARY.md +468 -0
  16. package/skills/cyberaudit/mobile/MOBILE-THREAT-MODELS.md +279 -0
  17. package/skills/cyberaudit/mobile/frameworks/EXPO.md +247 -0
  18. package/skills/cyberaudit/mobile/frameworks/FLUTTER.md +338 -0
  19. package/skills/cyberaudit/mobile/frameworks/IONIC.md +248 -0
  20. package/skills/cyberaudit/mobile/frameworks/REACT-NATIVE.md +479 -0
  21. package/skills/cyberaudit/mobile/vulnerabilities/AUTH-MOBILE.md +160 -0
  22. package/skills/cyberaudit/mobile/vulnerabilities/BINARY-ANALYSIS.md +193 -0
  23. package/skills/cyberaudit/mobile/vulnerabilities/CRYPTO-MOBILE.md +192 -0
  24. package/skills/cyberaudit/mobile/vulnerabilities/IPC-DEEPLINKS.md +231 -0
  25. package/skills/cyberaudit/mobile/vulnerabilities/NETWORK-MOBILE.md +165 -0
  26. package/skills/cyberaudit/mobile/vulnerabilities/PERMISSIONS.md +180 -0
  27. package/skills/cyberaudit/mobile/vulnerabilities/RUNTIME-MOBILE.md +214 -0
  28. package/skills/cyberaudit/mobile/vulnerabilities/STORAGE.md +197 -0
  29. package/skills/cyberaudit/reports/EXECUTIVE-SUMMARY-TEMPLATE.md +188 -0
  30. package/skills/cyberaudit/reports/REPORT-TEMPLATE-MOBILE.md +410 -0
  31. package/skills/cyberaudit/reports/REPORT-TEMPLATE-WEB.md +222 -0
  32. package/skills/cyberaudit/shared/COMPLIANCE.md +307 -0
  33. package/skills/cyberaudit/shared/CVSS-GUIDE.md +175 -0
  34. package/skills/cyberaudit/shared/OWASP-MAPPER.md +130 -0
  35. package/skills/cyberaudit/shared/SEVERITY-SCORING.md +102 -0
  36. package/skills/cyberaudit/shared/THREAT-MODELING.md +112 -0
  37. package/skills/cyberaudit/web/WEB-CHECKLIST.md +222 -0
  38. package/skills/cyberaudit/web/WEB-PHILOSOPHY.md +308 -0
  39. package/skills/cyberaudit/web/WEB-REMEDIATION-LIBRARY.md +665 -0
  40. package/skills/cyberaudit/web/WEB-THREAT-MODELS.md +460 -0
  41. package/skills/cyberaudit/web/frameworks/ANGULAR.md +169 -0
  42. package/skills/cyberaudit/web/frameworks/EXPRESS.md +185 -0
  43. package/skills/cyberaudit/web/frameworks/LARAVEL.md +371 -0
  44. package/skills/cyberaudit/web/frameworks/NESTJS.md +337 -0
  45. package/skills/cyberaudit/web/frameworks/NEXTJS.md +352 -0
  46. package/skills/cyberaudit/web/frameworks/REACT.md +346 -0
  47. package/skills/cyberaudit/web/frameworks/VUE.md +205 -0
  48. package/skills/cyberaudit/web/vulnerabilities/AUTH-AUTHZ.md +117 -0
  49. package/skills/cyberaudit/web/vulnerabilities/BUSINESS-LOGIC.md +603 -0
  50. package/skills/cyberaudit/web/vulnerabilities/CORS.md +117 -0
  51. package/skills/cyberaudit/web/vulnerabilities/CSRF.md +131 -0
  52. package/skills/cyberaudit/web/vulnerabilities/DESERIALIZATION.md +96 -0
  53. package/skills/cyberaudit/web/vulnerabilities/HEADERS.md +105 -0
  54. package/skills/cyberaudit/web/vulnerabilities/IDOR-BOLA.md +153 -0
  55. package/skills/cyberaudit/web/vulnerabilities/INJECTION.md +165 -0
  56. package/skills/cyberaudit/web/vulnerabilities/SECRETS.md +119 -0
  57. package/skills/cyberaudit/web/vulnerabilities/SSRF.md +124 -0
  58. package/skills/cyberaudit/web/vulnerabilities/SUPPLY-CHAIN.md +142 -0
  59. package/skills/cyberaudit/web/vulnerabilities/XSS.md +133 -0
  60. package/skills/cyberaudit/web/vulnerabilities/XXE.md +94 -0
@@ -0,0 +1,337 @@
1
+ # 🐈 NESTJS — SECURITY AUDIT GUIDE
2
+ # Comprehensive audit guide for NestJS applications
3
+
4
+ ═══════════════════════════════════════════════════════════════
5
+ NESTJS THREAT MODEL
6
+ ═══════════════════════════════════════════════════════════════
7
+
8
+ NESTJS ATTACK SURFACE
9
+ ─────────────────────────
10
+
11
+ LAYER 1 — CONTROLLERS
12
+ Risks: Endpoints without guards, poorly configured HTTP methods
13
+
14
+ LAYER 2 — DTOs AND VALIDATION
15
+ Risks: Missing validation, disabled whitelist,
16
+ untyped data passed directly
17
+
18
+ LAYER 3 — GUARDS AND INTERCEPTORS
19
+ Risks: Incomplete guards, incorrect order,
20
+ bypass via unprotected routes
21
+
22
+ LAYER 4 — SERVICES AND BUSINESS LOGIC
23
+ Risks: IDOR, authorization logic in the wrong layer
24
+
25
+ LAYER 5 — ORM (TYPEORM / PRISMA / MONGOOSE)
26
+ Risks: Raw queries, find() without ownership filter
27
+
28
+ LAYER 6 — GRAPHQL (if used)
29
+ Risks: Introspection in prod, query complexity, N+1
30
+
31
+ LAYER 7 — MICROSERVICES
32
+ Risks: Blind trust between internal services
33
+
34
+ ═══════════════════════════════════════════════════════════════
35
+ VULNERABLE NESTJS PATTERNS
36
+ ═══════════════════════════════════════════════════════════════
37
+
38
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
39
+ INSUFFICIENT VALIDATION
40
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
41
+
42
+ CRITICAL PATTERN — ValidationPipe without whitelist
43
+ DETECT:
44
+ // main.ts — dangerous configuration
45
+ app.useGlobalPipes(new ValidationPipe())
46
+ // Without whitelist: true → unknown fields pass through!
47
+
48
+ // Incomplete DTO
49
+ export class UpdateUserDto {
50
+ @IsString()
51
+ name: string
52
+ // role, isAdmin, balance not declared BUT accepted if whitelist absent!
53
+ }
54
+
55
+ FIX:
56
+ // main.ts — secure configuration
57
+ app.useGlobalPipes(new ValidationPipe({
58
+ whitelist: true, // Strips undeclared fields
59
+ forbidNonWhitelisted: true, // Error 400 on unknown field
60
+ transform: true, // Transforms according to TS types
61
+ transformOptions: {
62
+ enableImplicitConversion: false, // Explicit conversions only
63
+ },
64
+ disableErrorMessages: false, // Keep messages in dev
65
+ }))
66
+
67
+ // Secure DTO with explicit exclusions
68
+ export class UpdateUserDto {
69
+ @IsOptional()
70
+ @IsString()
71
+ @MinLength(2)
72
+ @MaxLength(100)
73
+ name?: string
74
+
75
+ @IsOptional()
76
+ @IsString()
77
+ @MaxLength(500)
78
+ bio?: string
79
+
80
+ // role, isAdmin, etc. = DO NOT DECLARE
81
+ // With whitelist: true, these fields will be automatically stripped
82
+ }
83
+
84
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
85
+ INSUFFICIENT GUARDS
86
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
87
+
88
+ CRITICAL PATTERN — Guard applied but bypassable
89
+ DETECT:
90
+ // Global guard but certain routes marked public
91
+ // without auditing those public routes
92
+
93
+ @Controller('admin')
94
+ export class AdminController {
95
+ @Get('stats')
96
+ // No guard → accessible to everyone!
97
+ getStats() { return this.statsService.getAll() }
98
+
99
+ @Post('users/:id/ban')
100
+ // Guard present but only checks auth, not role
101
+ @UseGuards(JwtAuthGuard)
102
+ banUser(@Param('id') id: string) { ... }
103
+ }
104
+
105
+ FIX:
106
+ // Global guard on the entire application
107
+ // app.module.ts
108
+ providers: [
109
+ { provide: APP_GUARD, useClass: JwtAuthGuard },
110
+ { provide: APP_GUARD, useClass: RolesGuard },
111
+ ]
112
+
113
+ // Decorator for public routes (explicit whitelist)
114
+ export const Public = () => SetMetadata('isPublic', true)
115
+
116
+ // JwtAuthGuard respecting @Public()
117
+ @Injectable()
118
+ export class JwtAuthGuard extends AuthGuard('jwt') {
119
+ constructor(private reflector: Reflector) { super() }
120
+
121
+ canActivate(context: ExecutionContext) {
122
+ const isPublic = this.reflector.getAllAndOverride<boolean>(
123
+ 'isPublic',
124
+ [context.getHandler(), context.getClass()]
125
+ )
126
+ if (isPublic) return true
127
+ return super.canActivate(context)
128
+ }
129
+ }
130
+
131
+ // Secure admin controller
132
+ @Controller('admin')
133
+ @Roles('ADMIN') // Role required on the entire controller
134
+ export class AdminController {
135
+
136
+ @Get('stats')
137
+ getStats() { return this.statsService.getAll() }
138
+
139
+ @Post('users/:id/ban')
140
+ @Roles('SUPER_ADMIN') // Higher role for this action
141
+ banUser(@Param('id') id: string) { ... }
142
+ }
143
+
144
+ // RolesGuard
145
+ @Injectable()
146
+ export class RolesGuard implements CanActivate {
147
+ constructor(private reflector: Reflector) {}
148
+
149
+ canActivate(context: ExecutionContext): boolean {
150
+ const requiredRoles = this.reflector.getAllAndOverride<string[]>(
151
+ 'roles',
152
+ [context.getHandler(), context.getClass()]
153
+ )
154
+ if (!requiredRoles?.length) return true
155
+
156
+ const { user } = context.switchToHttp().getRequest()
157
+ return requiredRoles.some(role => user?.roles?.includes(role))
158
+ }
159
+ }
160
+
161
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
162
+ IDOR VIA TYPEORM / PRISMA
163
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
164
+
165
+ CRITICAL PATTERN — findOne without ownership
166
+ DETECT:
167
+ // Vulnerable service
168
+ async getDocument(id: string): Promise<Document> {
169
+ return this.documentRepository.findOne({ where: { id } })
170
+ // No check that the user owns this document!
171
+ }
172
+
173
+ // Vulnerable Prisma
174
+ async getOrder(orderId: string) {
175
+ return this.prisma.order.findUnique({
176
+ where: { id: orderId }
177
+ // No userId in the where!
178
+ })
179
+ }
180
+
181
+ FIX:
182
+ // Secure service — TypeORM
183
+ async getDocument(
184
+ documentId: string,
185
+ requestingUserId: string
186
+ ): Promise<Document> {
187
+ const document = await this.documentRepository.findOne({
188
+ where: {
189
+ id: documentId,
190
+ userId: requestingUserId, // Ownership check in the query
191
+ }
192
+ })
193
+
194
+ if (!document) {
195
+ // 404 rather than 403 to avoid enumeration
196
+ throw new NotFoundException('Document not found')
197
+ }
198
+
199
+ return document
200
+ }
201
+
202
+ // Controller that passes userId from the token (not from body)
203
+ @Get(':id')
204
+ getDocument(
205
+ @Param('id') id: string,
206
+ @CurrentUser() user: User // From JWT, not from body
207
+ ) {
208
+ return this.documentService.getDocument(id, user.id)
209
+ }
210
+
211
+ // @CurrentUser() Decorator
212
+ export const CurrentUser = createParamDecorator(
213
+ (data: unknown, ctx: ExecutionContext) => {
214
+ const request = ctx.switchToHttp().getRequest()
215
+ return request.user // Set by JwtAuthGuard
216
+ }
217
+ )
218
+
219
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
220
+ GRAPHQL — SECURITY
221
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
222
+
223
+ HIGH PATTERN — Introspection and complexity not limited
224
+ DETECT:
225
+ GraphQLModule.forRoot({
226
+ autoSchemaFile: true,
227
+ // introspection not configured → enabled by default
228
+ // complexity not limited → DoS possible
229
+ })
230
+
231
+ FIX:
232
+ import { GraphQLModule } from '@nestjs/graphql'
233
+ import depthLimit from 'graphql-depth-limit'
234
+ import { createComplexityLimitRule } from 'graphql-validation-complexity'
235
+
236
+ GraphQLModule.forRoot<ApolloDriverConfig>({
237
+ driver: ApolloDriver,
238
+ autoSchemaFile: true,
239
+
240
+ // Disable introspection in production
241
+ introspection: process.env.NODE_ENV !== 'production',
242
+
243
+ // Limit query depth
244
+ validationRules: [
245
+ depthLimit(5),
246
+ createComplexityLimitRule(1000, {
247
+ onCost: (cost) => console.log('Query cost:', cost),
248
+ }),
249
+ ],
250
+
251
+ // Timeout on resolvers
252
+ context: ({ req }) => ({ req }),
253
+
254
+ // Disable playground in production
255
+ playground: process.env.NODE_ENV !== 'production',
256
+ })
257
+
258
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
259
+ TYPEORM RAW QUERIES
260
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
261
+
262
+ CRITICAL PATTERN — Injection via query() or createQueryBuilder
263
+ DETECT:
264
+ // Direct injection
265
+ await this.dataSource.query(
266
+ `SELECT * FROM users WHERE name = '${name}'`
267
+ )
268
+
269
+ // Template literal in QueryBuilder
270
+ await this.userRepository
271
+ .createQueryBuilder('user')
272
+ .where(`user.email = '${email}'`)
273
+ .getOne()
274
+
275
+ FIX:
276
+ // dataSource.query() with parameters
277
+ await this.dataSource.query(
278
+ 'SELECT * FROM users WHERE name = $1',
279
+ [name]
280
+ )
281
+
282
+ // QueryBuilder with named parameters
283
+ await this.userRepository
284
+ .createQueryBuilder('user')
285
+ .where('user.email = :email', { email })
286
+ .andWhere('user.isActive = :active', { active: true })
287
+ .getOne()
288
+
289
+ // Prisma — naturally secure except $queryRaw
290
+ // ❌ Dangerous:
291
+ await this.prisma.$queryRaw`
292
+ SELECT * FROM users WHERE name = ${name}
293
+ `
294
+ // ✅ Use Prisma.sql for parameters:
295
+ await this.prisma.$queryRaw(
296
+ Prisma.sql`SELECT * FROM users WHERE name = ${name}`
297
+ )
298
+ // Or even better, use standard Prisma methods:
299
+ await this.prisma.user.findFirst({ where: { name } })
300
+
301
+ ═══════════════════════════════════════════════════════════════
302
+ COMPLETE NESTJS CHECKLIST
303
+ ═══════════════════════════════════════════════════════════════
304
+
305
+ VALIDATION
306
+ □ Global ValidationPipe with whitelist: true?
307
+ □ forbidNonWhitelisted: true configured?
308
+ □ All DTOs with validation decorators?
309
+ □ transform: true for type conversion?
310
+
311
+ GUARDS AND AUTH
312
+ □ Global JwtAuthGuard on the entire application?
313
+ □ Public routes via @Public() (whitelist, not blacklist)?
314
+ □ RolesGuard for sensitive endpoints?
315
+ □ Guards tested with invalid/expired tokens?
316
+
317
+ AUTHORIZATION
318
+ □ UserId always retrieved from the token (not from body/params)?
319
+ □ Ownership verified in every resource query?
320
+ □ NotFoundException (not ForbiddenException) to avoid enumeration?
321
+
322
+ ORM
323
+ □ Raw queries with named parameters?
324
+ □ Prisma $queryRaw with Prisma.sql?
325
+ □ No interpolation in queries?
326
+
327
+ GRAPHQL (if applicable)
328
+ □ Introspection disabled in production?
329
+ □ Query depth limited?
330
+ □ Query complexity limited?
331
+ □ Playground disabled in production?
332
+
333
+ CONFIGURATION
334
+ □ Helmet configured?
335
+ □ CORS restrictive?
336
+ □ Rate limiting global + sensitive endpoints?
337
+ □ Secure compression?
@@ -0,0 +1,352 @@
1
+ # ▲ NEXT.JS — SECURITY AUDIT GUIDE
2
+ # Comprehensive audit guide for Next.js applications
3
+
4
+ ═══════════════════════════════════════════════════════════════
5
+ COMPLETE NEXT.JS THREAT MODEL
6
+ ═══════════════════════════════════════════════════════════════
7
+
8
+ NEXT.JS IS UNIQUE BECAUSE:
9
+ It is both frontend AND backend in the same project.
10
+ This duality creates context confusion risks.
11
+
12
+ "Does this code run client-side or server-side?"
13
+ This question is THE Next.js security question.
14
+
15
+ NEXT.JS ATTACK SURFACE
16
+ ─────────────────────────
17
+
18
+ LAYER 1 — API ROUTES (/api/*)
19
+ Context: Server only
20
+ Risks: Injection, auth bypass, IDOR, secret exposure
21
+
22
+ LAYER 2 — SERVER COMPONENTS (App Router)
23
+ Context: Server only
24
+ Risks: SSRF, injection, secret exposure in rendering
25
+
26
+ LAYER 3 — SERVER ACTIONS
27
+ Context: Server (called from client)
28
+ Risks: Auth bypass, injection, CSRF (before Next.js 14.1)
29
+
30
+ LAYER 4 — CLIENT COMPONENTS ("use client")
31
+ Context: Browser
32
+ Risks: XSS, secret exposure (NEXT_PUBLIC_),
33
+ localStorage abuse
34
+
35
+ LAYER 5 — getServerSideProps / getStaticProps
36
+ Context: Server
37
+ Risks: SSRF, injection, accidentally exposed secrets
38
+
39
+ LAYER 6 — MIDDLEWARE (middleware.ts)
40
+ Context: Edge Runtime
41
+ Risks: Auth bypass, incomplete protection
42
+
43
+ LAYER 7 — NEXT/IMAGE & NEXT/SCRIPT
44
+ Risks: SSRF via domains, XSS via third-party scripts
45
+
46
+ ═══════════════════════════════════════════════════════════════
47
+ VULNERABLE NEXT.JS PATTERNS
48
+ ═══════════════════════════════════════════════════════════════
49
+
50
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
51
+ SECRET EXPOSURE
52
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
53
+
54
+ CRITICAL PATTERN 1 — NEXT_PUBLIC_ with secrets
55
+ DETECT:
56
+ NEXT_PUBLIC_API_SECRET=sk-abc123
57
+ NEXT_PUBLIC_DATABASE_URL=postgresql://...
58
+ NEXT_PUBLIC_JWT_SECRET=supersecret
59
+ NEXT_PUBLIC_STRIPE_SECRET=sk_live_...
60
+
61
+ // In code:
62
+ const secret = process.env.NEXT_PUBLIC_MY_SECRET
63
+
64
+ UNDERSTAND:
65
+ NEXT_PUBLIC_ = injected into the JavaScript bundle
66
+ = visible in DevTools of any browser
67
+ = in the page's HTML source
68
+ = in the .js files downloaded by the client
69
+
70
+ FIX:
71
+ // Rule: NEXT_PUBLIC_ only for values
72
+ // that can be public without risk
73
+ NEXT_PUBLIC_APP_NAME=MyApp ✅ OK
74
+ NEXT_PUBLIC_API_URL=https://api.x.com ✅ OK (if URL is public)
75
+ NEXT_PUBLIC_STRIPE_PK=pk_live_... ✅ OK (Stripe public key)
76
+
77
+ // Secrets: without NEXT_PUBLIC_
78
+ DATABASE_URL=postgresql://... ✅ Server only
79
+ JWT_SECRET=... ✅ Server only
80
+ STRIPE_SECRET=sk_live_... ✅ Server only
81
+
82
+ // Verification in code:
83
+ // If you use a variable without NEXT_PUBLIC_
84
+ // in a Client Component → build error or undefined
85
+ // This is a security feature, not a bug
86
+
87
+ CRITICAL PATTERN 2 — Secrets returned to client in Server Actions
88
+ DETECT:
89
+ // Server Action
90
+ 'use server'
91
+ export async function getUserData() {
92
+ const user = await db.user.findUnique({...})
93
+ return user // Returns the ENTIRE user object!
94
+ }
95
+
96
+ FIX:
97
+ 'use server'
98
+ export async function getUserData() {
99
+ const user = await db.user.findUnique({
100
+ where: { id: getCurrentUserId() },
101
+ select: {
102
+ id: true,
103
+ name: true,
104
+ email: true,
105
+ // Do NOT include:
106
+ // password, apiKey, internalNotes, etc.
107
+ }
108
+ })
109
+ return user
110
+ }
111
+
112
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
113
+ MIDDLEWARE AUTH BYPASS
114
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
115
+
116
+ CRITICAL PATTERN 3 — Middleware as the only protection
117
+ DETECT:
118
+ // middleware.ts
119
+ export function middleware(request: NextRequest) {
120
+ const token = request.cookies.get('token')
121
+ if (!token) {
122
+ return NextResponse.redirect('/login')
123
+ }
124
+ // Lets through... but does NOT verify token validity!
125
+ }
126
+
127
+ // And in the API Route, no verification:
128
+ export async function GET(request: Request) {
129
+ // Assumes middleware already verified
130
+ const data = await db.sensitiveData.findMany()
131
+ return Response.json(data) // Vulnerable!
132
+ }
133
+
134
+ UNDERSTAND:
135
+ Middleware can be bypassed via direct API access.
136
+ Edge Runtime has limitations.
137
+ NEVER rely solely on middleware for data security.
138
+
139
+ FIX:
140
+ // middleware.ts — First filter (UX, not security alone)
141
+ export function middleware(request: NextRequest) {
142
+ const token = request.cookies.get('session-token')?.value
143
+
144
+ if (!token) {
145
+ return NextResponse.redirect(new URL('/login', request.url))
146
+ }
147
+
148
+ // Pass token to handler via header
149
+ const requestHeaders = new Headers(request.headers)
150
+ requestHeaders.set('x-user-token', token)
151
+
152
+ return NextResponse.next({ request: { headers: requestHeaders } })
153
+ }
154
+
155
+ // API Route — ALWAYS re-verify
156
+ export async function GET(request: Request) {
157
+ // Independent verification from middleware
158
+ const session = await getServerSession(authOptions)
159
+ if (!session) {
160
+ return new Response('Unauthorized', { status: 401 })
161
+ }
162
+
163
+ // Now secure
164
+ const data = await db.userData.findMany({
165
+ where: { userId: session.user.id } // IDOR protection
166
+ })
167
+ return Response.json(data)
168
+ }
169
+
170
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
171
+ SSRF VIA NEXT/IMAGE
172
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
173
+
174
+ HIGH PATTERN 4 — Overly permissive domains in next.config.js
175
+ DETECT:
176
+ // next.config.js
177
+ images: {
178
+ domains: ['*'], // CRITICAL
179
+ domains: ['**'], // CRITICAL
180
+ remotePatterns: [{
181
+ protocol: 'https',
182
+ hostname: '**', // CRITICAL
183
+ }],
184
+ remotePatterns: [{
185
+ hostname: '**.amazonaws.com', // Too broad
186
+ }]
187
+ }
188
+
189
+ UNDERSTAND:
190
+ Next.js proxies images through its server.
191
+ An overly permissive hostname = potential SSRF
192
+ The attacker can force the server to make requests
193
+ to internal resources.
194
+
195
+ FIX:
196
+ images: {
197
+ remotePatterns: [
198
+ {
199
+ protocol: 'https',
200
+ hostname: 'images.yourapp.com', // Exact domain
201
+ port: '',
202
+ pathname: '/uploads/**', // Limited path
203
+ },
204
+ {
205
+ protocol: 'https',
206
+ hostname: 'cdn.trusted-partner.com', // Known partner
207
+ },
208
+ ],
209
+ }
210
+
211
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
212
+ OPEN REDIRECT
213
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
214
+
215
+ HIGH PATTERN 5 — Redirect with unvalidated parameter
216
+ DETECT:
217
+ // API Route
218
+ const redirectUrl = searchParams.get('next') || '/'
219
+ return NextResponse.redirect(redirectUrl) // Vulnerable
220
+
221
+ // Pages Router
222
+ const { returnTo } = router.query
223
+ router.push(returnTo as string) // Vulnerable
224
+
225
+ // After login
226
+ redirect(searchParams.get('callbackUrl')) // Vulnerable
227
+
228
+ FIX:
229
+ function getSafeRedirectUrl(url: string | null): string {
230
+ if (!url) return '/'
231
+
232
+ try {
233
+ const parsed = new URL(url, 'https://yourapp.com')
234
+
235
+ // Only allow URLs from our domain
236
+ const allowedHosts = ['yourapp.com', 'www.yourapp.com']
237
+ if (!allowedHosts.includes(parsed.hostname)) {
238
+ return '/'
239
+ }
240
+
241
+ // Only allow known relative paths
242
+ const allowedPaths = ['/dashboard', '/profile', '/settings']
243
+ const pathAllowed = allowedPaths.some(p =>
244
+ parsed.pathname.startsWith(p)
245
+ )
246
+
247
+ return pathAllowed ? parsed.pathname : '/'
248
+ } catch {
249
+ // Invalid URL
250
+ return '/'
251
+ }
252
+ }
253
+
254
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
255
+ SERVER ACTIONS — CSRF AND VALIDATION
256
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
257
+
258
+ HIGH PATTERN 6 — Server Actions without validation or auth
259
+ DETECT:
260
+ 'use server'
261
+
262
+ export async function deleteUser(userId: string) {
263
+ // No auth check!
264
+ // No validation!
265
+ await db.user.delete({ where: { id: userId } })
266
+ }
267
+
268
+ export async function updateProfile(formData: FormData) {
269
+ // No verification that user is modifying THEIR profile
270
+ const id = formData.get('id')
271
+ await db.user.update({
272
+ where: { id: id as string }, // IDOR!
273
+ data: { name: formData.get('name') as string }
274
+ })
275
+ }
276
+
277
+ FIX:
278
+ 'use server'
279
+ import { auth } from '@/lib/auth'
280
+ import { z } from 'zod'
281
+
282
+ const UpdateProfileSchema = z.object({
283
+ name: z.string().min(1).max(100),
284
+ bio: z.string().max(500).optional(),
285
+ })
286
+
287
+ export async function updateProfile(formData: FormData) {
288
+ // 1. Verify authentication
289
+ const session = await auth()
290
+ if (!session?.user?.id) {
291
+ throw new Error('Not authenticated')
292
+ }
293
+
294
+ // 2. Validate data
295
+ const rawData = {
296
+ name: formData.get('name'),
297
+ bio: formData.get('bio'),
298
+ }
299
+
300
+ const validatedData = UpdateProfileSchema.safeParse(rawData)
301
+ if (!validatedData.success) {
302
+ throw new Error('Invalid data')
303
+ }
304
+
305
+ // 3. Update ONLY the logged-in user's profile
306
+ // No need for an ID provided by the client
307
+ await db.user.update({
308
+ where: { id: session.user.id }, // ID comes from session!
309
+ data: validatedData.data
310
+ })
311
+
312
+ revalidatePath('/profile')
313
+ }
314
+
315
+ ═══════════════════════════════════════════════════════════════
316
+ COMPLETE NEXT.JS CHECKLIST
317
+ ═══════════════════════════════════════════════════════════════
318
+
319
+ ENVIRONMENT VARIABLES
320
+ □ No secrets in NEXT_PUBLIC_?
321
+ □ .env.local in .gitignore?
322
+ □ Production variables managed via secrets manager?
323
+ □ Env vars validated at startup?
324
+
325
+ API ROUTES & SERVER ACTIONS
326
+ □ Auth verified independently from middleware?
327
+ □ IDOR: ownership verified on every resource?
328
+ □ Input validated with zod/yup/joi?
329
+ □ Rate limiting on sensitive endpoints?
330
+ □ HTTP methods verified? (GET ≠ POST)
331
+
332
+ MIDDLEWARE
333
+ □ Middleware is not the only security layer?
334
+ □ JWT token verified (signature + expiration) in middleware?
335
+ □ Matcher configured precisely?
336
+
337
+ RENDERING
338
+ □ dangerouslySetInnerHTML with DOMPurify if used?
339
+ □ User data escaped in JSX?
340
+ □ Source maps disabled in production?
341
+ (productionBrowserSourceMaps: false in next.config)
342
+
343
+ CONFIGURATION next.config.js
344
+ □ images.remotePatterns restrictive?
345
+ □ headers() configured with security headers?
346
+ □ rewrites() do not allow SSRF?
347
+ □ CSP configured via headers()?
348
+
349
+ DEPENDENCIES
350
+ □ next up to date (check security advisories)?
351
+ □ next-auth/auth.js configured correctly?
352
+ □ npm audit clean?