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.
- package/README.md +77 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +241 -0
- package/dist/mcp-server.d.ts +2 -0
- package/dist/mcp-server.js +126 -0
- package/package.json +58 -0
- package/skills/cyberaudit/AGENT-BOOT.md +122 -0
- package/skills/cyberaudit/COMMANDS.md +1125 -0
- package/skills/cyberaudit/INSTALL.md +311 -0
- package/skills/cyberaudit/MASTER.md +194 -0
- package/skills/cyberaudit/README.md +154 -0
- package/skills/cyberaudit/USAGE-GUIDE.md +107 -0
- package/skills/cyberaudit/mobile/MOBILE-CHECKLIST.md +245 -0
- package/skills/cyberaudit/mobile/MOBILE-PHILOSOPHY.md +352 -0
- package/skills/cyberaudit/mobile/MOBILE-REMEDIATION-LIBRARY.md +468 -0
- package/skills/cyberaudit/mobile/MOBILE-THREAT-MODELS.md +279 -0
- package/skills/cyberaudit/mobile/frameworks/EXPO.md +247 -0
- package/skills/cyberaudit/mobile/frameworks/FLUTTER.md +338 -0
- package/skills/cyberaudit/mobile/frameworks/IONIC.md +248 -0
- package/skills/cyberaudit/mobile/frameworks/REACT-NATIVE.md +479 -0
- package/skills/cyberaudit/mobile/vulnerabilities/AUTH-MOBILE.md +160 -0
- package/skills/cyberaudit/mobile/vulnerabilities/BINARY-ANALYSIS.md +193 -0
- package/skills/cyberaudit/mobile/vulnerabilities/CRYPTO-MOBILE.md +192 -0
- package/skills/cyberaudit/mobile/vulnerabilities/IPC-DEEPLINKS.md +231 -0
- package/skills/cyberaudit/mobile/vulnerabilities/NETWORK-MOBILE.md +165 -0
- package/skills/cyberaudit/mobile/vulnerabilities/PERMISSIONS.md +180 -0
- package/skills/cyberaudit/mobile/vulnerabilities/RUNTIME-MOBILE.md +214 -0
- package/skills/cyberaudit/mobile/vulnerabilities/STORAGE.md +197 -0
- package/skills/cyberaudit/reports/EXECUTIVE-SUMMARY-TEMPLATE.md +188 -0
- package/skills/cyberaudit/reports/REPORT-TEMPLATE-MOBILE.md +410 -0
- package/skills/cyberaudit/reports/REPORT-TEMPLATE-WEB.md +222 -0
- package/skills/cyberaudit/shared/COMPLIANCE.md +307 -0
- package/skills/cyberaudit/shared/CVSS-GUIDE.md +175 -0
- package/skills/cyberaudit/shared/OWASP-MAPPER.md +130 -0
- package/skills/cyberaudit/shared/SEVERITY-SCORING.md +102 -0
- package/skills/cyberaudit/shared/THREAT-MODELING.md +112 -0
- package/skills/cyberaudit/web/WEB-CHECKLIST.md +222 -0
- package/skills/cyberaudit/web/WEB-PHILOSOPHY.md +308 -0
- package/skills/cyberaudit/web/WEB-REMEDIATION-LIBRARY.md +665 -0
- package/skills/cyberaudit/web/WEB-THREAT-MODELS.md +460 -0
- package/skills/cyberaudit/web/frameworks/ANGULAR.md +169 -0
- package/skills/cyberaudit/web/frameworks/EXPRESS.md +185 -0
- package/skills/cyberaudit/web/frameworks/LARAVEL.md +371 -0
- package/skills/cyberaudit/web/frameworks/NESTJS.md +337 -0
- package/skills/cyberaudit/web/frameworks/NEXTJS.md +352 -0
- package/skills/cyberaudit/web/frameworks/REACT.md +346 -0
- package/skills/cyberaudit/web/frameworks/VUE.md +205 -0
- package/skills/cyberaudit/web/vulnerabilities/AUTH-AUTHZ.md +117 -0
- package/skills/cyberaudit/web/vulnerabilities/BUSINESS-LOGIC.md +603 -0
- package/skills/cyberaudit/web/vulnerabilities/CORS.md +117 -0
- package/skills/cyberaudit/web/vulnerabilities/CSRF.md +131 -0
- package/skills/cyberaudit/web/vulnerabilities/DESERIALIZATION.md +96 -0
- package/skills/cyberaudit/web/vulnerabilities/HEADERS.md +105 -0
- package/skills/cyberaudit/web/vulnerabilities/IDOR-BOLA.md +153 -0
- package/skills/cyberaudit/web/vulnerabilities/INJECTION.md +165 -0
- package/skills/cyberaudit/web/vulnerabilities/SECRETS.md +119 -0
- package/skills/cyberaudit/web/vulnerabilities/SSRF.md +124 -0
- package/skills/cyberaudit/web/vulnerabilities/SUPPLY-CHAIN.md +142 -0
- package/skills/cyberaudit/web/vulnerabilities/XSS.md +133 -0
- 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?
|