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,185 @@
|
|
|
1
|
+
# 🚂 EXPRESS — SECURITY AUDIT GUIDE
|
|
2
|
+
|
|
3
|
+
═══════════════════════════════════════════════════════════════
|
|
4
|
+
EXPRESS THREAT MODEL
|
|
5
|
+
═══════════════════════════════════════════════════════════════
|
|
6
|
+
|
|
7
|
+
EXPRESS IS MINIMALIST.
|
|
8
|
+
What is a strength is also a danger.
|
|
9
|
+
No default protection.
|
|
10
|
+
Everything must be explicitly configured.
|
|
11
|
+
An Express audit = verify that everything was added manually.
|
|
12
|
+
|
|
13
|
+
SPECIFIC EXPRESS RISKS:
|
|
14
|
+
→ No automatic input validation
|
|
15
|
+
→ No built-in auth
|
|
16
|
+
→ No CSRF protection by default
|
|
17
|
+
→ Middleware in wrong order = bypass
|
|
18
|
+
→ next() called after error = dangerous continuation
|
|
19
|
+
|
|
20
|
+
═══════════════════════════════════════════════════════════════
|
|
21
|
+
VULNERABLE EXPRESS PATTERNS
|
|
22
|
+
═══════════════════════════════════════════════════════════════
|
|
23
|
+
|
|
24
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
25
|
+
MIDDLEWARE ORDER
|
|
26
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
27
|
+
|
|
28
|
+
CRITICAL PATTERN — Auth middleware after routes
|
|
29
|
+
DETECT:
|
|
30
|
+
app.get('/admin/users', getUsers) // Route before middleware!
|
|
31
|
+
app.use(authMiddleware) // Too late!
|
|
32
|
+
|
|
33
|
+
FIX:
|
|
34
|
+
// Order matters: middleware first, routes next
|
|
35
|
+
app.use(helmet())
|
|
36
|
+
app.use(cors(corsOptions))
|
|
37
|
+
app.use(express.json({ limit: '10mb' }))
|
|
38
|
+
app.use(rateLimiter)
|
|
39
|
+
app.use(authMiddleware) // Auth BEFORE protected routes
|
|
40
|
+
|
|
41
|
+
// Public routes
|
|
42
|
+
app.post('/api/auth/login', loginHandler)
|
|
43
|
+
app.post('/api/auth/register', registerHandler)
|
|
44
|
+
|
|
45
|
+
// Protected routes (after auth middleware)
|
|
46
|
+
app.use('/api/admin', requireAdmin)
|
|
47
|
+
app.get('/api/admin/users', getUsers)
|
|
48
|
+
|
|
49
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
50
|
+
NEXT() AFTER AN ERROR
|
|
51
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
52
|
+
|
|
53
|
+
HIGH PATTERN — Continuation after auth error
|
|
54
|
+
DETECT:
|
|
55
|
+
const authMiddleware = (req, res, next) => {
|
|
56
|
+
const token = req.headers.authorization?.split(' ')[1]
|
|
57
|
+
if (!token) {
|
|
58
|
+
res.status(401).json({ error: 'No token' })
|
|
59
|
+
next() // ← CRITICAL BUG: continues despite the error!
|
|
60
|
+
}
|
|
61
|
+
// ...
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
FIX:
|
|
65
|
+
const authMiddleware = (req, res, next) => {
|
|
66
|
+
const token = req.headers.authorization?.split(' ')[1]
|
|
67
|
+
|
|
68
|
+
if (!token) {
|
|
69
|
+
return res.status(401).json({ error: 'Unauthorized' })
|
|
70
|
+
// return → do not call next()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const payload = jwt.verify(token, process.env.JWT_SECRET, {
|
|
75
|
+
algorithms: ['HS256']
|
|
76
|
+
})
|
|
77
|
+
req.user = payload
|
|
78
|
+
next() // Called only if auth succeeds
|
|
79
|
+
} catch (error) {
|
|
80
|
+
return res.status(401).json({ error: 'Invalid token' })
|
|
81
|
+
// return → do not call next()
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
86
|
+
COMPLETE SECURE CONFIGURATION
|
|
87
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
88
|
+
|
|
89
|
+
SECURE EXPRESS APP
|
|
90
|
+
import express from 'express'
|
|
91
|
+
import helmet from 'helmet'
|
|
92
|
+
import cors from 'cors'
|
|
93
|
+
import rateLimit from 'express-rate-limit'
|
|
94
|
+
import mongoSanitize from 'express-mongo-sanitize'
|
|
95
|
+
import hpp from 'hpp'
|
|
96
|
+
|
|
97
|
+
const app = express()
|
|
98
|
+
|
|
99
|
+
// 1. Security headers
|
|
100
|
+
app.use(helmet({
|
|
101
|
+
contentSecurityPolicy: {
|
|
102
|
+
directives: {
|
|
103
|
+
defaultSrc: ["'self'"],
|
|
104
|
+
scriptSrc: ["'self'"],
|
|
105
|
+
styleSrc: ["'self'"],
|
|
106
|
+
imgSrc: ["'self'", 'data:'],
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
hsts: { maxAge: 31536000, includeSubDomains: true, preload: true }
|
|
110
|
+
}))
|
|
111
|
+
|
|
112
|
+
// 2. Remove X-Powered-By
|
|
113
|
+
app.disable('x-powered-by')
|
|
114
|
+
|
|
115
|
+
// 3. Restrictive CORS
|
|
116
|
+
app.use(cors({
|
|
117
|
+
origin: ['https://yourapp.com'],
|
|
118
|
+
credentials: true,
|
|
119
|
+
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
|
|
120
|
+
}))
|
|
121
|
+
|
|
122
|
+
// 4. Body parsing with limits
|
|
123
|
+
app.use(express.json({ limit: '10mb' }))
|
|
124
|
+
app.use(express.urlencoded({ extended: true, limit: '10mb' }))
|
|
125
|
+
|
|
126
|
+
// 5. NoSQL injection sanitization
|
|
127
|
+
app.use(mongoSanitize())
|
|
128
|
+
|
|
129
|
+
// 6. HPP (HTTP Parameter Pollution) protection
|
|
130
|
+
app.use(hpp())
|
|
131
|
+
|
|
132
|
+
// 7. Global rate limiting
|
|
133
|
+
app.use(rateLimit({
|
|
134
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
135
|
+
max: 100,
|
|
136
|
+
standardHeaders: true,
|
|
137
|
+
legacyHeaders: false,
|
|
138
|
+
message: { error: 'Too many requests, try again later' }
|
|
139
|
+
}))
|
|
140
|
+
|
|
141
|
+
// 8. Specific rate limiting on auth
|
|
142
|
+
const authLimiter = rateLimit({
|
|
143
|
+
windowMs: 60 * 1000,
|
|
144
|
+
max: 5,
|
|
145
|
+
skipSuccessfulRequests: true,
|
|
146
|
+
})
|
|
147
|
+
app.use('/api/auth/login', authLimiter)
|
|
148
|
+
app.use('/api/auth/forgot-password', authLimiter)
|
|
149
|
+
|
|
150
|
+
// 9. Public routes
|
|
151
|
+
app.post('/api/auth/login', loginHandler)
|
|
152
|
+
|
|
153
|
+
// 10. Auth middleware for protected routes
|
|
154
|
+
app.use('/api', authMiddleware)
|
|
155
|
+
|
|
156
|
+
// 11. Protected routes
|
|
157
|
+
app.use('/api/users', userRouter)
|
|
158
|
+
app.use('/api/admin', adminMiddleware, adminRouter)
|
|
159
|
+
|
|
160
|
+
// 12. Global error handler (must be last)
|
|
161
|
+
app.use((err, req, res, next) => {
|
|
162
|
+
console.error(err) // Full log server-side
|
|
163
|
+
res.status(err.status || 500).json({
|
|
164
|
+
error: process.env.NODE_ENV === 'production'
|
|
165
|
+
? 'An error occurred'
|
|
166
|
+
: err.message
|
|
167
|
+
})
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
═══════════════════════════════════════════════════════════════
|
|
171
|
+
COMPLETE EXPRESS CHECKLIST
|
|
172
|
+
═══════════════════════════════════════════════════════════════
|
|
173
|
+
|
|
174
|
+
□ Helmet configured first?
|
|
175
|
+
□ X-Powered-By disabled?
|
|
176
|
+
□ CORS with origin whitelist?
|
|
177
|
+
□ Body size limited?
|
|
178
|
+
□ Global + auth rate limiting?
|
|
179
|
+
□ Middleware in correct order?
|
|
180
|
+
□ next() never called after res.json() on error?
|
|
181
|
+
□ Global error handler last?
|
|
182
|
+
□ No error details exposed in prod?
|
|
183
|
+
□ mongoSanitize() if MongoDB used?
|
|
184
|
+
□ hpp() to prevent parameter pollution?
|
|
185
|
+
□ Input validation (joi, zod, express-validator)?
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
# 🐘 LARAVEL — SECURITY AUDIT GUIDE
|
|
2
|
+
# Comprehensive audit guide for Laravel applications
|
|
3
|
+
|
|
4
|
+
═══════════════════════════════════════════════════════════════
|
|
5
|
+
COMPLETE LARAVEL THREAT MODEL
|
|
6
|
+
═══════════════════════════════════════════════════════════════
|
|
7
|
+
|
|
8
|
+
LARAVEL ATTACK SURFACE
|
|
9
|
+
──────────────────────────
|
|
10
|
+
|
|
11
|
+
LAYER 1 — ROUTES & MIDDLEWARE
|
|
12
|
+
Risks: Routes without auth, bypassed middleware,
|
|
13
|
+
rate limiting absent, CSRF disabled
|
|
14
|
+
|
|
15
|
+
LAYER 2 — CONTROLLERS & REQUESTS
|
|
16
|
+
Risks: Insufficient validation, mass assignment,
|
|
17
|
+
injection via request()->all()
|
|
18
|
+
|
|
19
|
+
LAYER 3 — ELOQUENT & DATABASE
|
|
20
|
+
Risks: Raw queries, whereRaw injection,
|
|
21
|
+
N+1 not critical but revealing
|
|
22
|
+
|
|
23
|
+
LAYER 4 — BLADE TEMPLATES
|
|
24
|
+
Risks: {!! !!} unsanitized, XSS via output
|
|
25
|
+
|
|
26
|
+
LAYER 5 — FILE SYSTEM
|
|
27
|
+
Risks: Upload path traversal, public storage
|
|
28
|
+
of sensitive files, MIME bypass
|
|
29
|
+
|
|
30
|
+
LAYER 6 — AUTHENTICATION
|
|
31
|
+
Risks: Auth bypass, mass assignment on User,
|
|
32
|
+
poorly implemented remember_me
|
|
33
|
+
|
|
34
|
+
LAYER 7 — CONFIGURATION
|
|
35
|
+
Risks: APP_DEBUG=true in prod, exposed .env,
|
|
36
|
+
weak encryption keys
|
|
37
|
+
|
|
38
|
+
LAYER 8 — DESERIALIZATION
|
|
39
|
+
Risks: unserialize() with user input,
|
|
40
|
+
Laravel gadget chains
|
|
41
|
+
|
|
42
|
+
═══════════════════════════════════════════════════════════════
|
|
43
|
+
VULNERABLE LARAVEL PATTERNS
|
|
44
|
+
═══════════════════════════════════════════════════════════════
|
|
45
|
+
|
|
46
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
47
|
+
SQL INJECTION
|
|
48
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
49
|
+
|
|
50
|
+
CRITICAL PATTERN 1 — DB::select with interpolation
|
|
51
|
+
DETECT:
|
|
52
|
+
DB::select("SELECT * FROM users WHERE id = $id")
|
|
53
|
+
DB::select("SELECT * FROM users WHERE email = '{$email}'")
|
|
54
|
+
DB::statement("DELETE FROM logs WHERE date < $date")
|
|
55
|
+
|
|
56
|
+
FIX:
|
|
57
|
+
DB::select("SELECT * FROM users WHERE id = ?", [$id])
|
|
58
|
+
DB::select("SELECT * FROM users WHERE email = ?", [$email])
|
|
59
|
+
// OR better with Eloquent
|
|
60
|
+
User::where('id', $id)->first()
|
|
61
|
+
User::where('email', $email)->first()
|
|
62
|
+
|
|
63
|
+
CRITICAL PATTERN 2 — whereRaw with direct variable
|
|
64
|
+
DETECT:
|
|
65
|
+
Model::whereRaw("name = '$name'")->get()
|
|
66
|
+
Model::whereRaw("status = " . $status)->get()
|
|
67
|
+
Model::whereRaw($request->filter)->get()
|
|
68
|
+
|
|
69
|
+
FIX:
|
|
70
|
+
Model::whereRaw("name = ?", [$name])->get()
|
|
71
|
+
Model::whereRaw("status = ?", [$status])->get()
|
|
72
|
+
// Validate allowed filters first
|
|
73
|
+
$allowedFilters = ['active', 'inactive', 'pending'];
|
|
74
|
+
if (!in_array($request->filter, $allowedFilters)) {
|
|
75
|
+
abort(400);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
CRITICAL PATTERN 3 — orderByRaw with user input
|
|
79
|
+
DETECT:
|
|
80
|
+
Model::orderByRaw($request->sort)->get()
|
|
81
|
+
Model::orderByRaw($request->column . ' ' . $request->direction)
|
|
82
|
+
|
|
83
|
+
FIX:
|
|
84
|
+
$allowedColumns = ['name', 'email', 'created_at', 'updated_at'];
|
|
85
|
+
$allowedDirections = ['asc', 'desc'];
|
|
86
|
+
|
|
87
|
+
$column = in_array($request->sort, $allowedColumns)
|
|
88
|
+
? $request->sort : 'created_at';
|
|
89
|
+
$direction = in_array($request->direction, $allowedDirections)
|
|
90
|
+
? $request->direction : 'desc';
|
|
91
|
+
|
|
92
|
+
Model::orderBy($column, $direction)->get()
|
|
93
|
+
|
|
94
|
+
HIGH PATTERN 4 — havingRaw and groupByRaw
|
|
95
|
+
DETECT:
|
|
96
|
+
->havingRaw($request->having)
|
|
97
|
+
->groupByRaw($request->group)
|
|
98
|
+
|
|
99
|
+
FIX:
|
|
100
|
+
// Whitelist of allowed expressions
|
|
101
|
+
$allowedHaving = ['COUNT(*) > 5', 'SUM(amount) > 1000'];
|
|
102
|
+
// Never pass user input directly
|
|
103
|
+
|
|
104
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
105
|
+
MASS ASSIGNMENT
|
|
106
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
107
|
+
|
|
108
|
+
CRITICAL PATTERN 5 — create() with request()->all()
|
|
109
|
+
DETECT:
|
|
110
|
+
User::create($request->all())
|
|
111
|
+
Post::create($request->input())
|
|
112
|
+
$model->fill($request->all())
|
|
113
|
+
$model->update($request->all())
|
|
114
|
+
|
|
115
|
+
FIX:
|
|
116
|
+
// Option A: strict $fillable
|
|
117
|
+
// In the Model:
|
|
118
|
+
protected $fillable = ['name', 'email', 'bio'];
|
|
119
|
+
// NEVER: protected $guarded = []; // everything allowed
|
|
120
|
+
|
|
121
|
+
// Option B: validated() with FormRequest
|
|
122
|
+
User::create($request->validated())
|
|
123
|
+
|
|
124
|
+
// Option C: manual selection
|
|
125
|
+
User::create([
|
|
126
|
+
'name' => $request->name,
|
|
127
|
+
'email' => $request->email,
|
|
128
|
+
])
|
|
129
|
+
|
|
130
|
+
// VERIFY these fields are NOT in $fillable:
|
|
131
|
+
// role, is_admin, is_verified, email_verified_at,
|
|
132
|
+
// balance, credits, stripe_id
|
|
133
|
+
|
|
134
|
+
CRITICAL PATTERN 6 — update() without whitelist
|
|
135
|
+
DETECT:
|
|
136
|
+
$user->update($request->all())
|
|
137
|
+
$user->update($data) // if $data comes from input
|
|
138
|
+
|
|
139
|
+
FIX:
|
|
140
|
+
$user->update($request->only(['name', 'bio', 'avatar']))
|
|
141
|
+
// OR
|
|
142
|
+
$user->update($request->validated())
|
|
143
|
+
|
|
144
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
145
|
+
BLADE XSS
|
|
146
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
147
|
+
|
|
148
|
+
HIGH PATTERN 7 — Unescaped output
|
|
149
|
+
DETECT:
|
|
150
|
+
{!! $userContent !!}
|
|
151
|
+
{!! $comment->body !!}
|
|
152
|
+
{!! $user->bio !!}
|
|
153
|
+
<?php echo $input; ?>
|
|
154
|
+
|
|
155
|
+
FIX:
|
|
156
|
+
// For plain text: always {{ }}
|
|
157
|
+
{{ $userContent }}
|
|
158
|
+
|
|
159
|
+
// For trusted HTML (admin only):
|
|
160
|
+
// Use HTMLPurifier or Sterilize
|
|
161
|
+
{!! clean($userContent) !!}
|
|
162
|
+
|
|
163
|
+
// Install: composer require mews/purifier
|
|
164
|
+
// Config: config/purifier.php
|
|
165
|
+
|
|
166
|
+
MEDIUM PATTERN 8 — href with unvalidated variable
|
|
167
|
+
DETECT:
|
|
168
|
+
<a href="{{ $user->website }}">
|
|
169
|
+
<a href="{!! $url !!}">
|
|
170
|
+
<script src="{{ $cdnUrl }}">
|
|
171
|
+
|
|
172
|
+
FIX:
|
|
173
|
+
// Validate it is indeed an HTTP/HTTPS URL
|
|
174
|
+
@php
|
|
175
|
+
$safeUrl = filter_var($user->website, FILTER_VALIDATE_URL)
|
|
176
|
+
&& in_array(parse_url($user->website, PHP_URL_SCHEME), ['http', 'https'])
|
|
177
|
+
? $user->website : '#';
|
|
178
|
+
@endphp
|
|
179
|
+
<a href="{{ $safeUrl }}">
|
|
180
|
+
|
|
181
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
182
|
+
IDOR AND AUTHORIZATION
|
|
183
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
184
|
+
|
|
185
|
+
CRITICAL PATTERN 9 — Access without ownership check
|
|
186
|
+
DETECT:
|
|
187
|
+
public function show($id) {
|
|
188
|
+
$order = Order::find($id); // No ownership check!
|
|
189
|
+
return $order;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
public function update($id, Request $request) {
|
|
193
|
+
$post = Post::findOrFail($id); // Vulnerable
|
|
194
|
+
$post->update($request->validated());
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
FIX:
|
|
198
|
+
// Option A: where() with user_id
|
|
199
|
+
public function show($id) {
|
|
200
|
+
$order = Order::where('id', $id)
|
|
201
|
+
->where('user_id', auth()->id())
|
|
202
|
+
->firstOrFail();
|
|
203
|
+
return $order;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Option B: Route Model Binding with Policy
|
|
207
|
+
public function update(Post $post, UpdatePostRequest $request) {
|
|
208
|
+
$this->authorize('update', $post); // Laravel Policy
|
|
209
|
+
$post->update($request->validated());
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Option C: Policy in the controller
|
|
213
|
+
public function destroy(Order $order) {
|
|
214
|
+
if ($order->user_id !== auth()->id()) {
|
|
215
|
+
abort(403, 'Unauthorized action.');
|
|
216
|
+
}
|
|
217
|
+
$order->delete();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
221
|
+
FILE UPLOAD
|
|
222
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
223
|
+
|
|
224
|
+
CRITICAL PATTERN 10 — Upload without MIME validation
|
|
225
|
+
DETECT:
|
|
226
|
+
$file = $request->file('avatar');
|
|
227
|
+
$file->store('uploads'); // No validation!
|
|
228
|
+
|
|
229
|
+
$extension = $request->file('doc')->getClientOriginalExtension();
|
|
230
|
+
// getClientOriginalExtension() is spoofable!
|
|
231
|
+
|
|
232
|
+
FIX:
|
|
233
|
+
use Illuminate\Support\Facades\Storage;
|
|
234
|
+
|
|
235
|
+
public function uploadAvatar(Request $request) {
|
|
236
|
+
$request->validate([
|
|
237
|
+
'avatar' => [
|
|
238
|
+
'required',
|
|
239
|
+
'file',
|
|
240
|
+
'max:2048', // 2MB max
|
|
241
|
+
'mimes:jpeg,png,gif,webp', // Extension whitelist
|
|
242
|
+
// Real MIME verification (not just extension)
|
|
243
|
+
function ($attribute, $value, $fail) {
|
|
244
|
+
$mimeType = $value->getMimeType();
|
|
245
|
+
$allowedMimes = ['image/jpeg', 'image/png',
|
|
246
|
+
'image/gif', 'image/webp'];
|
|
247
|
+
if (!in_array($mimeType, $allowedMimes)) {
|
|
248
|
+
$fail('File type not allowed.');
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
],
|
|
252
|
+
]);
|
|
253
|
+
|
|
254
|
+
// Generate a random name (never use the original name)
|
|
255
|
+
$filename = Str::random(40) . '.' .
|
|
256
|
+
$request->file('avatar')->extension();
|
|
257
|
+
|
|
258
|
+
// Store OUTSIDE the public folder if possible
|
|
259
|
+
$path = $request->file('avatar')
|
|
260
|
+
->storeAs('avatars', $filename, 'private');
|
|
261
|
+
|
|
262
|
+
// Serve via a controlled route
|
|
263
|
+
// GET /avatar/{filename} → check rights → return the file
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
267
|
+
CRITICAL CONFIGURATION
|
|
268
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
269
|
+
|
|
270
|
+
CRITICAL PATTERN 11 — APP_DEBUG in production
|
|
271
|
+
DETECT in .env or config/app.php:
|
|
272
|
+
APP_DEBUG=true
|
|
273
|
+
'debug' => true
|
|
274
|
+
'debug' => env('APP_DEBUG', true) // dangerous default
|
|
275
|
+
|
|
276
|
+
FIX:
|
|
277
|
+
// .env production
|
|
278
|
+
APP_DEBUG=false
|
|
279
|
+
|
|
280
|
+
// config/app.php
|
|
281
|
+
'debug' => (bool) env('APP_DEBUG', false),
|
|
282
|
+
|
|
283
|
+
// Startup verification possible:
|
|
284
|
+
// AppServiceProvider::boot()
|
|
285
|
+
if (app()->isProduction() && config('app.debug')) {
|
|
286
|
+
throw new \RuntimeException(
|
|
287
|
+
'DEBUG MODE IS ENABLED IN PRODUCTION!'
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
CRITICAL PATTERN 12 — Weak or default application key
|
|
292
|
+
DETECT in .env:
|
|
293
|
+
APP_KEY= (empty)
|
|
294
|
+
APP_KEY=base64:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
|
|
295
|
+
APP_KEY=SomeEasyToGuessString
|
|
296
|
+
|
|
297
|
+
FIX:
|
|
298
|
+
// Generate a real key
|
|
299
|
+
php artisan key:generate
|
|
300
|
+
|
|
301
|
+
// Verify length (must be 32 bytes in base64)
|
|
302
|
+
// Generated key starts with: base64:
|
|
303
|
+
// and is exactly 44 characters after the prefix
|
|
304
|
+
|
|
305
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
306
|
+
DESERIALIZATION
|
|
307
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
308
|
+
|
|
309
|
+
CRITICAL PATTERN 13 — unserialize() with user input
|
|
310
|
+
DETECT:
|
|
311
|
+
unserialize($request->input('data'))
|
|
312
|
+
unserialize(base64_decode($token))
|
|
313
|
+
unserialize(Cookie::get('cart'))
|
|
314
|
+
|
|
315
|
+
FIX:
|
|
316
|
+
// NEVER unserialize() user data
|
|
317
|
+
// Use JSON instead
|
|
318
|
+
$data = json_decode($request->input('data'), true);
|
|
319
|
+
|
|
320
|
+
// For sensitive cookies: Laravel encryption
|
|
321
|
+
// (enabled by default via EncryptCookies middleware)
|
|
322
|
+
|
|
323
|
+
// If serialize() is necessary for internal cache:
|
|
324
|
+
// Ensure the data NEVER comes from the user
|
|
325
|
+
|
|
326
|
+
═══════════════════════════════════════════════════════════════
|
|
327
|
+
COMPLETE LARAVEL CHECKLIST
|
|
328
|
+
═══════════════════════════════════════════════════════════════
|
|
329
|
+
|
|
330
|
+
ROUTES & MIDDLEWARE
|
|
331
|
+
□ All sensitive routes protected by auth middleware?
|
|
332
|
+
□ CSRF token present on all POST forms?
|
|
333
|
+
□ CSRF disabled only on webhooks with signature validation?
|
|
334
|
+
□ Rate limiting configured on login/register/reset-password?
|
|
335
|
+
□ API throttle configured?
|
|
336
|
+
□ Laravel debug routes disabled in prod?
|
|
337
|
+
|
|
338
|
+
ELOQUENT MODELS
|
|
339
|
+
□ $fillable defined on ALL models?
|
|
340
|
+
□ $guarded = [] absent?
|
|
341
|
+
□ Sensitive fields in $hidden? (password, remember_token)
|
|
342
|
+
□ Sensitive fields in $casts? (password → hashed)
|
|
343
|
+
□ N+1 relations not exploitable for IDOR?
|
|
344
|
+
|
|
345
|
+
CONTROLLERS
|
|
346
|
+
□ FormRequest used for validation?
|
|
347
|
+
□ authorize() called in FormRequests?
|
|
348
|
+
□ Ownership verified before every object access?
|
|
349
|
+
□ Response does not return unnecessary sensitive data?
|
|
350
|
+
|
|
351
|
+
AUTHENTICATION
|
|
352
|
+
□ Password hashing: Hash::make() used?
|
|
353
|
+
□ Comparison: Hash::check() used?
|
|
354
|
+
□ Remember me: limited duration?
|
|
355
|
+
□ Sessions invalidated on logout?
|
|
356
|
+
□ Email verification enabled?
|
|
357
|
+
□ Sanctum/Passport configured correctly?
|
|
358
|
+
|
|
359
|
+
CONFIGURATION
|
|
360
|
+
□ APP_DEBUG=false in production?
|
|
361
|
+
□ APP_ENV=production in production?
|
|
362
|
+
□ APP_KEY randomly generated?
|
|
363
|
+
□ .env in .gitignore?
|
|
364
|
+
□ config:cache used in prod?
|
|
365
|
+
□ Logs do not contain sensitive data?
|
|
366
|
+
□ Exceptions handler does not reveal infrastructure?
|
|
367
|
+
|
|
368
|
+
DEPENDENCIES
|
|
369
|
+
□ composer audit executed?
|
|
370
|
+
□ Packages up to date?
|
|
371
|
+
□ composer.lock committed?
|