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,665 @@
|
|
|
1
|
+
# 🔧 WEB REMEDIATION LIBRARY — CYBERAUDIT SKILL
|
|
2
|
+
# Comprehensive library of ready-to-use fixes
|
|
3
|
+
|
|
4
|
+
═══════════════════════════════════════════════════════════════
|
|
5
|
+
USAGE: When a finding is identified, pick the corresponding
|
|
6
|
+
remediation code here.
|
|
7
|
+
Each remediation is tested and production-ready.
|
|
8
|
+
═══════════════════════════════════════════════════════════════
|
|
9
|
+
|
|
10
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
11
|
+
REM-001 — UNIVERSAL INPUT VALIDATION
|
|
12
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
13
|
+
|
|
14
|
+
LARAVEL — Full FormRequest
|
|
15
|
+
class StoreUserRequest extends FormRequest
|
|
16
|
+
{
|
|
17
|
+
public function authorize(): bool
|
|
18
|
+
{
|
|
19
|
+
return auth()->check();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public function rules(): array
|
|
23
|
+
{
|
|
24
|
+
return [
|
|
25
|
+
'name' => ['required', 'string', 'min:2', 'max:100',
|
|
26
|
+
'regex:/^[\p{L}\s\-]+$/u'],
|
|
27
|
+
'email' => ['required', 'email:rfc,dns', 'max:255',
|
|
28
|
+
'unique:users,email'],
|
|
29
|
+
'age' => ['required', 'integer', 'min:18', 'max:120'],
|
|
30
|
+
'website' => ['nullable', 'url', 'max:255',
|
|
31
|
+
'regex:/^https?:\/\//'],
|
|
32
|
+
'bio' => ['nullable', 'string', 'max:1000'],
|
|
33
|
+
'role' => ['prohibited'], // Never from the client
|
|
34
|
+
];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
public function messages(): array
|
|
38
|
+
{
|
|
39
|
+
return [
|
|
40
|
+
'email.unique' => 'This email is already in use.',
|
|
41
|
+
// Generic messages to avoid enumeration
|
|
42
|
+
];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
NESTJS — DTO with class-validator
|
|
47
|
+
import { IsString, IsEmail, MinLength, MaxLength,
|
|
48
|
+
Matches, IsOptional, IsUrl } from 'class-validator'
|
|
49
|
+
import { Transform } from 'class-transformer'
|
|
50
|
+
|
|
51
|
+
export class CreateUserDto {
|
|
52
|
+
@IsString()
|
|
53
|
+
@MinLength(2)
|
|
54
|
+
@MaxLength(100)
|
|
55
|
+
@Matches(/^[\p{L}\s\-]+$/u, {
|
|
56
|
+
message: 'Name can only contain letters'
|
|
57
|
+
})
|
|
58
|
+
@Transform(({ value }) => value?.trim())
|
|
59
|
+
name: string
|
|
60
|
+
|
|
61
|
+
@IsEmail()
|
|
62
|
+
@MaxLength(255)
|
|
63
|
+
@Transform(({ value }) => value?.toLowerCase().trim())
|
|
64
|
+
email: string
|
|
65
|
+
|
|
66
|
+
@IsString()
|
|
67
|
+
@MinLength(12)
|
|
68
|
+
@MaxLength(128)
|
|
69
|
+
@Matches(
|
|
70
|
+
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])/,
|
|
71
|
+
{ message: 'Password insufficiently complex' }
|
|
72
|
+
)
|
|
73
|
+
password: string
|
|
74
|
+
|
|
75
|
+
@IsOptional()
|
|
76
|
+
@IsUrl({ protocols: ['https'] })
|
|
77
|
+
website?: string
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// In the main module:
|
|
81
|
+
// app.useGlobalPipes(new ValidationPipe({
|
|
82
|
+
// whitelist: true, // Strips undeclared fields
|
|
83
|
+
// forbidNonWhitelisted: true, // Error on unknown fields
|
|
84
|
+
// transform: true, // Transforms according to DTO types
|
|
85
|
+
// }))
|
|
86
|
+
|
|
87
|
+
NEXTJS — Validation with Zod
|
|
88
|
+
import { z } from 'zod'
|
|
89
|
+
|
|
90
|
+
const UserSchema = z.object({
|
|
91
|
+
name: z.string()
|
|
92
|
+
.min(2, 'Name too short')
|
|
93
|
+
.max(100, 'Name too long')
|
|
94
|
+
.regex(/^[\p{L}\s\-]+$/u, 'Invalid characters'),
|
|
95
|
+
|
|
96
|
+
email: z.string()
|
|
97
|
+
.email('Invalid email')
|
|
98
|
+
.max(255)
|
|
99
|
+
.toLowerCase()
|
|
100
|
+
.trim(),
|
|
101
|
+
|
|
102
|
+
password: z.string()
|
|
103
|
+
.min(12, 'Minimum 12 characters')
|
|
104
|
+
.regex(/[A-Z]/, 'At least one uppercase')
|
|
105
|
+
.regex(/[0-9]/, 'At least one digit')
|
|
106
|
+
.regex(/[^A-Za-z0-9]/, 'At least one special character'),
|
|
107
|
+
|
|
108
|
+
website: z.string()
|
|
109
|
+
.url()
|
|
110
|
+
.startsWith('https://')
|
|
111
|
+
.optional(),
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
// Usage in a Server Action or API Route:
|
|
115
|
+
export async function createUser(data: unknown) {
|
|
116
|
+
const validated = UserSchema.safeParse(data)
|
|
117
|
+
if (!validated.success) {
|
|
118
|
+
return { error: validated.error.flatten() }
|
|
119
|
+
}
|
|
120
|
+
// Now validated.data is typed and safe
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
124
|
+
REM-002 — SECURE PASSWORD HASHING
|
|
125
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
126
|
+
|
|
127
|
+
NODE.JS — Argon2 (recommended)
|
|
128
|
+
import argon2 from 'argon2'
|
|
129
|
+
|
|
130
|
+
// Hash
|
|
131
|
+
const hashedPassword = await argon2.hash(plainPassword, {
|
|
132
|
+
type: argon2.argon2id, // Resistant to timing AND side-channel
|
|
133
|
+
memoryCost: 65536, // 64 MB
|
|
134
|
+
timeCost: 3, // 3 iterations
|
|
135
|
+
parallelism: 4, // 4 threads
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
// Verify
|
|
139
|
+
const isValid = await argon2.verify(hashedPassword, plainPassword)
|
|
140
|
+
|
|
141
|
+
NODE.JS — bcrypt (acceptable)
|
|
142
|
+
import bcrypt from 'bcrypt'
|
|
143
|
+
const SALT_ROUNDS = 12 // Minimum 12 in production
|
|
144
|
+
|
|
145
|
+
const hashedPassword = await bcrypt.hash(plainPassword, SALT_ROUNDS)
|
|
146
|
+
const isValid = await bcrypt.compare(plainPassword, hashedPassword)
|
|
147
|
+
|
|
148
|
+
LARAVEL
|
|
149
|
+
// Hash (automatic with $casts)
|
|
150
|
+
protected $casts = ['password' => 'hashed'];
|
|
151
|
+
|
|
152
|
+
// Or manually
|
|
153
|
+
$hashedPassword = Hash::make($plainPassword);
|
|
154
|
+
|
|
155
|
+
// Verify
|
|
156
|
+
$isValid = Hash::check($plainPassword, $hashedPassword);
|
|
157
|
+
|
|
158
|
+
// Check if rehash needed (after config change)
|
|
159
|
+
if (Hash::needsRehash($user->password)) {
|
|
160
|
+
$user->update(['password' => Hash::make($newPassword)]);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
164
|
+
REM-003 — SECURE JWT
|
|
165
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
166
|
+
|
|
167
|
+
NODE.JS — Full configuration
|
|
168
|
+
import jwt from 'jsonwebtoken'
|
|
169
|
+
import crypto from 'crypto'
|
|
170
|
+
|
|
171
|
+
// Generate a strong secret (do once)
|
|
172
|
+
// node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
|
|
173
|
+
|
|
174
|
+
const JWT_CONFIG = {
|
|
175
|
+
accessToken: {
|
|
176
|
+
secret: process.env.JWT_ACCESS_SECRET, // 64+ random bytes
|
|
177
|
+
expiresIn: '15m',
|
|
178
|
+
algorithm: 'HS256' as const,
|
|
179
|
+
},
|
|
180
|
+
refreshToken: {
|
|
181
|
+
secret: process.env.JWT_REFRESH_SECRET, // Different secret!
|
|
182
|
+
expiresIn: '7d',
|
|
183
|
+
algorithm: 'HS256' as const,
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Create an access token
|
|
188
|
+
function createAccessToken(userId: string, role: string): string {
|
|
189
|
+
return jwt.sign(
|
|
190
|
+
{
|
|
191
|
+
sub: userId,
|
|
192
|
+
role: role,
|
|
193
|
+
type: 'access',
|
|
194
|
+
jti: crypto.randomUUID(), // Unique token ID
|
|
195
|
+
},
|
|
196
|
+
JWT_CONFIG.accessToken.secret!,
|
|
197
|
+
{
|
|
198
|
+
expiresIn: JWT_CONFIG.accessToken.expiresIn,
|
|
199
|
+
algorithm: JWT_CONFIG.accessToken.algorithm,
|
|
200
|
+
issuer: 'yourapp.com',
|
|
201
|
+
audience: 'yourapp-api',
|
|
202
|
+
}
|
|
203
|
+
)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Verify a token
|
|
207
|
+
function verifyAccessToken(token: string) {
|
|
208
|
+
try {
|
|
209
|
+
return jwt.verify(token, JWT_CONFIG.accessToken.secret!, {
|
|
210
|
+
algorithms: ['HS256'], // NEVER leave 'none'
|
|
211
|
+
issuer: 'yourapp.com',
|
|
212
|
+
audience: 'yourapp-api',
|
|
213
|
+
})
|
|
214
|
+
} catch (error) {
|
|
215
|
+
// TokenExpiredError, JsonWebTokenError, NotBeforeError
|
|
216
|
+
throw new UnauthorizedException('Invalid token')
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
221
|
+
REM-004 — COMPLETE SECURITY HEADERS
|
|
222
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
223
|
+
|
|
224
|
+
NEXT.JS — next.config.js
|
|
225
|
+
const securityHeaders = [
|
|
226
|
+
{
|
|
227
|
+
key: 'Content-Security-Policy',
|
|
228
|
+
value: [
|
|
229
|
+
"default-src 'self'",
|
|
230
|
+
"script-src 'self' 'nonce-{NONCE}'",
|
|
231
|
+
"style-src 'self' 'unsafe-inline'",
|
|
232
|
+
"img-src 'self' data: https:",
|
|
233
|
+
"font-src 'self'",
|
|
234
|
+
"connect-src 'self' https://api.yourapp.com",
|
|
235
|
+
"frame-ancestors 'none'",
|
|
236
|
+
"base-uri 'self'",
|
|
237
|
+
"form-action 'self'",
|
|
238
|
+
"upgrade-insecure-requests",
|
|
239
|
+
].join('; ')
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
key: 'Strict-Transport-Security',
|
|
243
|
+
value: 'max-age=31536000; includeSubDomains; preload'
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
key: 'X-Frame-Options',
|
|
247
|
+
value: 'DENY'
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
key: 'X-Content-Type-Options',
|
|
251
|
+
value: 'nosniff'
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
key: 'Referrer-Policy',
|
|
255
|
+
value: 'strict-origin-when-cross-origin'
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
key: 'Permissions-Policy',
|
|
259
|
+
value: 'camera=(), microphone=(), geolocation=(), payment=()'
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
key: 'X-DNS-Prefetch-Control',
|
|
263
|
+
value: 'on'
|
|
264
|
+
}
|
|
265
|
+
]
|
|
266
|
+
|
|
267
|
+
module.exports = {
|
|
268
|
+
async headers() {
|
|
269
|
+
return [
|
|
270
|
+
{
|
|
271
|
+
source: '/(.*)',
|
|
272
|
+
headers: securityHeaders,
|
|
273
|
+
},
|
|
274
|
+
]
|
|
275
|
+
},
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
LARAVEL — Middleware
|
|
279
|
+
class SecurityHeadersMiddleware
|
|
280
|
+
{
|
|
281
|
+
public function handle(Request $request, Closure $next): Response
|
|
282
|
+
{
|
|
283
|
+
$response = $next($request);
|
|
284
|
+
|
|
285
|
+
$response->headers->set(
|
|
286
|
+
'Content-Security-Policy',
|
|
287
|
+
"default-src 'self'; " .
|
|
288
|
+
"script-src 'self'; " .
|
|
289
|
+
"style-src 'self' 'unsafe-inline'; " .
|
|
290
|
+
"img-src 'self' data: https:; " .
|
|
291
|
+
"frame-ancestors 'none'; " .
|
|
292
|
+
"base-uri 'self'"
|
|
293
|
+
);
|
|
294
|
+
$response->headers->set(
|
|
295
|
+
'Strict-Transport-Security',
|
|
296
|
+
'max-age=31536000; includeSubDomains; preload'
|
|
297
|
+
);
|
|
298
|
+
$response->headers->set('X-Frame-Options', 'DENY');
|
|
299
|
+
$response->headers->set('X-Content-Type-Options', 'nosniff');
|
|
300
|
+
$response->headers->set(
|
|
301
|
+
'Referrer-Policy',
|
|
302
|
+
'strict-origin-when-cross-origin'
|
|
303
|
+
);
|
|
304
|
+
$response->headers->set(
|
|
305
|
+
'Permissions-Policy',
|
|
306
|
+
'camera=(), microphone=(), geolocation=()'
|
|
307
|
+
);
|
|
308
|
+
$response->headers->remove('X-Powered-By');
|
|
309
|
+
$response->headers->remove('Server');
|
|
310
|
+
|
|
311
|
+
return $response;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
NESTJS — Helmet
|
|
316
|
+
import helmet from '@fastify/helmet'
|
|
317
|
+
// OR for Express:
|
|
318
|
+
import helmet from 'helmet'
|
|
319
|
+
|
|
320
|
+
app.use(helmet({
|
|
321
|
+
contentSecurityPolicy: {
|
|
322
|
+
directives: {
|
|
323
|
+
defaultSrc: ["'self'"],
|
|
324
|
+
scriptSrc: ["'self'"],
|
|
325
|
+
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
326
|
+
imgSrc: ["'self'", 'data:', 'https:'],
|
|
327
|
+
frameAncestors: ["'none'"],
|
|
328
|
+
baseUri: ["'self'"],
|
|
329
|
+
formAction: ["'self'"],
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
hsts: {
|
|
333
|
+
maxAge: 31536000,
|
|
334
|
+
includeSubDomains: true,
|
|
335
|
+
preload: true,
|
|
336
|
+
},
|
|
337
|
+
frameguard: { action: 'deny' },
|
|
338
|
+
noSniff: true,
|
|
339
|
+
referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
|
|
340
|
+
}))
|
|
341
|
+
|
|
342
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
343
|
+
REM-005 — RATE LIMITING
|
|
344
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
345
|
+
|
|
346
|
+
NESTJS — ThrottlerModule
|
|
347
|
+
// app.module.ts
|
|
348
|
+
ThrottlerModule.forRoot([
|
|
349
|
+
{ name: 'global', ttl: 60000, limit: 100 },
|
|
350
|
+
])
|
|
351
|
+
|
|
352
|
+
// On sensitive endpoints:
|
|
353
|
+
@Throttle({ default: { ttl: 60000, limit: 5 } })
|
|
354
|
+
@Post('auth/login')
|
|
355
|
+
login(@Body() dto: LoginDto) { ... }
|
|
356
|
+
|
|
357
|
+
@Throttle({ default: { ttl: 3600000, limit: 3 } })
|
|
358
|
+
@Post('auth/forgot-password')
|
|
359
|
+
forgotPassword(@Body() dto: ForgotPasswordDto) { ... }
|
|
360
|
+
|
|
361
|
+
LARAVEL
|
|
362
|
+
// routes/api.php
|
|
363
|
+
Route::middleware(['throttle:login'])->group(function () {
|
|
364
|
+
Route::post('/login', [AuthController::class, 'login']);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
// app/Providers/RouteServiceProvider.php
|
|
368
|
+
RateLimiter::for('login', function (Request $request) {
|
|
369
|
+
return [
|
|
370
|
+
Limit::perMinute(5)->by($request->ip()),
|
|
371
|
+
Limit::perMinute(10)->by($request->input('email')),
|
|
372
|
+
];
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
RateLimiter::for('api', function (Request $request) {
|
|
376
|
+
return Limit::perMinute(60)->by(
|
|
377
|
+
$request->user()?->id ?: $request->ip()
|
|
378
|
+
);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
NEXT.JS — with upstash/ratelimit
|
|
382
|
+
import { Ratelimit } from '@upstash/ratelimit'
|
|
383
|
+
import { Redis } from '@upstash/redis'
|
|
384
|
+
|
|
385
|
+
const ratelimit = new Ratelimit({
|
|
386
|
+
redis: Redis.fromEnv(),
|
|
387
|
+
limiter: Ratelimit.slidingWindow(5, '60 s'),
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
export async function POST(request: Request) {
|
|
391
|
+
const ip = request.headers.get('x-forwarded-for') ?? 'anonymous'
|
|
392
|
+
const { success, limit, remaining } = await ratelimit.limit(ip)
|
|
393
|
+
|
|
394
|
+
if (!success) {
|
|
395
|
+
return new Response('Too Many Requests', {
|
|
396
|
+
status: 429,
|
|
397
|
+
headers: {
|
|
398
|
+
'X-RateLimit-Limit': limit.toString(),
|
|
399
|
+
'X-RateLimit-Remaining': remaining.toString(),
|
|
400
|
+
'Retry-After': '60',
|
|
401
|
+
}
|
|
402
|
+
})
|
|
403
|
+
}
|
|
404
|
+
// Continue processing
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
408
|
+
REM-006 — SECURE CORS
|
|
409
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
410
|
+
|
|
411
|
+
NESTJS
|
|
412
|
+
app.enableCors({
|
|
413
|
+
origin: (origin, callback) => {
|
|
414
|
+
const allowedOrigins = [
|
|
415
|
+
'https://yourapp.com',
|
|
416
|
+
'https://www.yourapp.com',
|
|
417
|
+
process.env.NODE_ENV === 'development'
|
|
418
|
+
? 'http://localhost:3000'
|
|
419
|
+
: null,
|
|
420
|
+
].filter(Boolean)
|
|
421
|
+
|
|
422
|
+
if (!origin || allowedOrigins.includes(origin)) {
|
|
423
|
+
callback(null, true)
|
|
424
|
+
} else {
|
|
425
|
+
callback(new Error(`CORS: origin ${origin} not allowed`))
|
|
426
|
+
}
|
|
427
|
+
},
|
|
428
|
+
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
|
|
429
|
+
allowedHeaders: ['Content-Type', 'Authorization'],
|
|
430
|
+
credentials: true, // Allows cookies
|
|
431
|
+
maxAge: 86400, // Cache preflight 24h
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
LARAVEL — config/cors.php
|
|
435
|
+
return [
|
|
436
|
+
'paths' => ['api/*'],
|
|
437
|
+
'allowed_methods' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
|
|
438
|
+
'allowed_origins' => [env('FRONTEND_URL', 'https://yourapp.com')],
|
|
439
|
+
'allowed_origins_patterns' => [],
|
|
440
|
+
'allowed_headers' => ['Content-Type', 'Authorization', 'X-Requested-With'],
|
|
441
|
+
'exposed_headers' => [],
|
|
442
|
+
'max_age' => 86400,
|
|
443
|
+
'supports_credentials' => true,
|
|
444
|
+
];
|
|
445
|
+
|
|
446
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
447
|
+
REM-007 — SECURE ERROR HANDLING
|
|
448
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
449
|
+
|
|
450
|
+
NESTJS — Global Exception Filter
|
|
451
|
+
@Catch()
|
|
452
|
+
export class GlobalExceptionFilter implements ExceptionFilter {
|
|
453
|
+
catch(exception: unknown, host: ArgumentsHost) {
|
|
454
|
+
const ctx = host.switchToHttp()
|
|
455
|
+
const response = ctx.getResponse()
|
|
456
|
+
const request = ctx.getRequest()
|
|
457
|
+
|
|
458
|
+
const status = exception instanceof HttpException
|
|
459
|
+
? exception.getStatus()
|
|
460
|
+
: HttpStatus.INTERNAL_SERVER_ERROR
|
|
461
|
+
|
|
462
|
+
// Full log for internal monitoring
|
|
463
|
+
console.error({
|
|
464
|
+
timestamp: new Date().toISOString(),
|
|
465
|
+
path: request.url,
|
|
466
|
+
exception: exception,
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
// Minimal response for the client
|
|
470
|
+
// Never expose internal details
|
|
471
|
+
response.status(status).json({
|
|
472
|
+
statusCode: status,
|
|
473
|
+
message: status === 500
|
|
474
|
+
? 'An internal error occurred'
|
|
475
|
+
: (exception as HttpException).message,
|
|
476
|
+
// No stack trace, no infrastructure details
|
|
477
|
+
})
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
LARAVEL — app/Exceptions/Handler.php
|
|
482
|
+
public function render($request, Throwable $exception): Response
|
|
483
|
+
{
|
|
484
|
+
if ($request->expectsJson()) {
|
|
485
|
+
if ($exception instanceof ValidationException) {
|
|
486
|
+
return response()->json([
|
|
487
|
+
'message' => 'Invalid data',
|
|
488
|
+
'errors' => $exception->errors(),
|
|
489
|
+
], 422);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if ($exception instanceof ModelNotFoundException) {
|
|
493
|
+
return response()->json([
|
|
494
|
+
'message' => 'Resource not found',
|
|
495
|
+
], 404);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Generic error — do not expose details
|
|
499
|
+
$statusCode = method_exists($exception, 'getStatusCode')
|
|
500
|
+
? $exception->getStatusCode() : 500;
|
|
501
|
+
|
|
502
|
+
return response()->json([
|
|
503
|
+
'message' => app()->isProduction()
|
|
504
|
+
? 'An error occurred'
|
|
505
|
+
: $exception->getMessage(),
|
|
506
|
+
], $statusCode);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
return parent::render($request, $exception);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
513
|
+
REM-008 — SECURE UPLOAD
|
|
514
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
515
|
+
|
|
516
|
+
NESTJS — Full validation pipe
|
|
517
|
+
import { FileInterceptor } from '@nestjs/platform-express'
|
|
518
|
+
import { memoryStorage } from 'multer'
|
|
519
|
+
import { fromBuffer } from 'file-type'
|
|
520
|
+
import sharp from 'sharp'
|
|
521
|
+
import { randomBytes } from 'crypto'
|
|
522
|
+
|
|
523
|
+
@Post('upload')
|
|
524
|
+
@UseInterceptors(FileInterceptor('file', {
|
|
525
|
+
storage: memoryStorage(),
|
|
526
|
+
limits: {
|
|
527
|
+
fileSize: 5 * 1024 * 1024, // 5MB max
|
|
528
|
+
files: 1,
|
|
529
|
+
},
|
|
530
|
+
}))
|
|
531
|
+
async uploadFile(@UploadedFile() file: Express.Multer.File) {
|
|
532
|
+
|
|
533
|
+
// 1. Verify real MIME type (magic bytes)
|
|
534
|
+
const fileType = await fromBuffer(file.buffer)
|
|
535
|
+
const allowedMimes = ['image/jpeg', 'image/png', 'image/webp']
|
|
536
|
+
|
|
537
|
+
if (!fileType || !allowedMimes.includes(fileType.mime)) {
|
|
538
|
+
throw new BadRequestException('File type not allowed')
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// 2. Reprocess image (strips metadata and payloads)
|
|
542
|
+
const safeBuffer = await sharp(file.buffer)
|
|
543
|
+
.resize(1920, 1080, { fit: 'inside', withoutEnlargement: true })
|
|
544
|
+
.jpeg({ quality: 85 })
|
|
545
|
+
.toBuffer()
|
|
546
|
+
|
|
547
|
+
// 3. Random filename
|
|
548
|
+
const filename = `${randomBytes(32).toString('hex')}.jpg`
|
|
549
|
+
|
|
550
|
+
// 4. Store in a private bucket (not web-accessible)
|
|
551
|
+
// await s3.upload({ Bucket: 'private-uploads', Key: filename, ... })
|
|
552
|
+
|
|
553
|
+
// 5. Return a signed URL or controlled route
|
|
554
|
+
return { url: `/api/files/${filename}` }
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
558
|
+
REM-009 — SECURE LOGGING
|
|
559
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
560
|
+
|
|
561
|
+
UNIVERSAL PATTERN — What should be logged
|
|
562
|
+
// ALWAYS log:
|
|
563
|
+
{
|
|
564
|
+
timestamp: "2024-01-15T10:30:00Z",
|
|
565
|
+
event_type: "auth.login.success" | "auth.login.failure" |
|
|
566
|
+
"resource.access" | "resource.modify" | "resource.delete" |
|
|
567
|
+
"permission.denied" | "rate_limit.exceeded",
|
|
568
|
+
user_id: "uuid-of-the-user", // Never name/email
|
|
569
|
+
ip_address: "x.x.x.x", // Hashed if strict GDPR
|
|
570
|
+
user_agent: "Mozilla/5.0...",
|
|
571
|
+
resource: "/api/orders/uuid-xxx",
|
|
572
|
+
action: "GET",
|
|
573
|
+
result: "success" | "failure",
|
|
574
|
+
// NEVER: password, token, credit_card, ssn, etc.
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
NODE.JS — Winston configured
|
|
578
|
+
import winston from 'winston'
|
|
579
|
+
|
|
580
|
+
const logger = winston.createLogger({
|
|
581
|
+
level: process.env.NODE_ENV === 'production' ? 'warn' : 'debug',
|
|
582
|
+
format: winston.format.combine(
|
|
583
|
+
winston.format.timestamp(),
|
|
584
|
+
winston.format.errors({ stack: false }), // No stack in prod
|
|
585
|
+
winston.format.json(),
|
|
586
|
+
),
|
|
587
|
+
transports: [
|
|
588
|
+
new winston.transports.File({
|
|
589
|
+
filename: 'logs/security.log',
|
|
590
|
+
level: 'warn',
|
|
591
|
+
}),
|
|
592
|
+
new winston.transports.Console({
|
|
593
|
+
silent: process.env.NODE_ENV === 'production',
|
|
594
|
+
}),
|
|
595
|
+
],
|
|
596
|
+
})
|
|
597
|
+
|
|
598
|
+
// Audit middleware
|
|
599
|
+
function auditLog(event: string, userId: string,
|
|
600
|
+
resource: string, result: string) {
|
|
601
|
+
logger.info({
|
|
602
|
+
event_type: event,
|
|
603
|
+
user_id: userId,
|
|
604
|
+
resource: resource,
|
|
605
|
+
result: result,
|
|
606
|
+
timestamp: new Date().toISOString(),
|
|
607
|
+
// No sensitive data here
|
|
608
|
+
})
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
612
|
+
REM-010 — ENVIRONMENT VARIABLES
|
|
613
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
614
|
+
|
|
615
|
+
STARTUP VALIDATION — Node.js with Zod
|
|
616
|
+
import { z } from 'zod'
|
|
617
|
+
|
|
618
|
+
const EnvSchema = z.object({
|
|
619
|
+
NODE_ENV: z.enum(['development', 'staging', 'production']),
|
|
620
|
+
DATABASE_URL: z.string().url().startsWith('postgresql://'),
|
|
621
|
+
JWT_ACCESS_SECRET: z.string().min(64),
|
|
622
|
+
JWT_REFRESH_SECRET:z.string().min(64),
|
|
623
|
+
REDIS_URL: z.string().url(),
|
|
624
|
+
APP_URL: z.string().url().startsWith('https://'),
|
|
625
|
+
|
|
626
|
+
// Keys that MUST NOT be there
|
|
627
|
+
// (if someone adds them by mistake, error at startup)
|
|
628
|
+
PASSWORD: z.undefined(),
|
|
629
|
+
SECRET: z.undefined(),
|
|
630
|
+
})
|
|
631
|
+
|
|
632
|
+
const env = EnvSchema.safeParse(process.env)
|
|
633
|
+
|
|
634
|
+
if (!env.success) {
|
|
635
|
+
console.error('❌ Invalid environment variables:')
|
|
636
|
+
console.error(env.error.flatten().fieldErrors)
|
|
637
|
+
process.exit(1) // Refuse to start
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
export const config = env.data
|
|
641
|
+
|
|
642
|
+
STARTUP VALIDATION — Laravel
|
|
643
|
+
// app/Providers/AppServiceProvider.php
|
|
644
|
+
public function boot(): void
|
|
645
|
+
{
|
|
646
|
+
$requiredVars = [
|
|
647
|
+
'APP_KEY', 'DB_CONNECTION', 'DB_HOST',
|
|
648
|
+
'DB_DATABASE', 'DB_USERNAME', 'DB_PASSWORD',
|
|
649
|
+
'JWT_SECRET',
|
|
650
|
+
];
|
|
651
|
+
|
|
652
|
+
foreach ($requiredVars as $var) {
|
|
653
|
+
if (empty(env($var))) {
|
|
654
|
+
throw new \RuntimeException(
|
|
655
|
+
"Missing environment variable: {$var}"
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
if (app()->isProduction() && config('app.debug')) {
|
|
661
|
+
throw new \RuntimeException(
|
|
662
|
+
'APP_DEBUG must be false in production!'
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
}
|