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,603 @@
|
|
|
1
|
+
# 🧠 BUSINESS LOGIC — CYBERAUDIT SKILL
|
|
2
|
+
# Business logic vulnerabilities — The hardest to detect
|
|
3
|
+
|
|
4
|
+
═══════════════════════════════════════════════════════════════
|
|
5
|
+
NATURE OF THESE VULNERABILITIES :
|
|
6
|
+
|
|
7
|
+
Business logic flaws are not detectable
|
|
8
|
+
by automated scanners.
|
|
9
|
+
They require understanding WHAT THE APP IS SUPPOSED TO DO
|
|
10
|
+
to identify WHAT THE APP ACTUALLY DOES.
|
|
11
|
+
|
|
12
|
+
A scanner sees code. An expert sees intent.
|
|
13
|
+
These flaws require being the expert.
|
|
14
|
+
═══════════════════════════════════════════════════════════════
|
|
15
|
+
|
|
16
|
+
═══════════════════════════════════════════════════════════════
|
|
17
|
+
BUSINESS LOGIC FLAW CATEGORIES
|
|
18
|
+
═══════════════════════════════════════════════════════════════
|
|
19
|
+
|
|
20
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
21
|
+
CATEGORY 1 — PRICE AND VALUE MANIPULATION
|
|
22
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
23
|
+
|
|
24
|
+
PRINCIPLE :
|
|
25
|
+
Price, quantity, amount must NEVER
|
|
26
|
+
come from the client. Always recalculated server-side.
|
|
27
|
+
|
|
28
|
+
CRITICAL PATTERN 1 — Price sent by client
|
|
29
|
+
DETECT :
|
|
30
|
+
// Frontend sends the price
|
|
31
|
+
POST /api/checkout
|
|
32
|
+
{
|
|
33
|
+
"product_id": 42,
|
|
34
|
+
"quantity": 1,
|
|
35
|
+
"price": 9.99 ← Price provided by client!
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// And the backend uses it directly
|
|
39
|
+
public function checkout(Request $request) {
|
|
40
|
+
$order = Order::create([
|
|
41
|
+
'product_id' => $request->product_id,
|
|
42
|
+
'quantity' => $request->quantity,
|
|
43
|
+
'price' => $request->price, // ← VULNERABLE
|
|
44
|
+
'total' => $request->price * $request->quantity
|
|
45
|
+
]);
|
|
46
|
+
$this->processPayment($order->total);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
EXPLOITATION SCENARIO :
|
|
50
|
+
Attacker modifies request with Burp Suite :
|
|
51
|
+
"price": 0.01 → Buys a 999€ product for 0.01€
|
|
52
|
+
"price": -1 → Receives a refund on purchase
|
|
53
|
+
"quantity": -1 → Possible inverted logic
|
|
54
|
+
|
|
55
|
+
FIX :
|
|
56
|
+
public function checkout(Request $request) {
|
|
57
|
+
// Never trust the client's price
|
|
58
|
+
// ALWAYS recalculate server-side
|
|
59
|
+
$product = Product::findOrFail($request->product_id);
|
|
60
|
+
|
|
61
|
+
// Verify product is available
|
|
62
|
+
if (!$product->is_available) {
|
|
63
|
+
return response()->json(['error' => 'Product unavailable'], 400);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Validate quantity
|
|
67
|
+
$quantity = (int) $request->quantity;
|
|
68
|
+
if ($quantity < 1 || $quantity > 100) {
|
|
69
|
+
return response()->json(['error' => 'Invalid quantity'], 400);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Calculate price SERVER-SIDE only
|
|
73
|
+
$unitPrice = $product->current_price; // Price from DB
|
|
74
|
+
$discount = $this->calculateDiscount($product, auth()->user());
|
|
75
|
+
$total = ($unitPrice * $quantity) * (1 - $discount);
|
|
76
|
+
|
|
77
|
+
$order = Order::create([
|
|
78
|
+
'user_id' => auth()->id(),
|
|
79
|
+
'product_id' => $product->id,
|
|
80
|
+
'quantity' => $quantity,
|
|
81
|
+
'unit_price' => $unitPrice, // Server price
|
|
82
|
+
'discount' => $discount,
|
|
83
|
+
'total' => $total, // Server-calculated
|
|
84
|
+
]);
|
|
85
|
+
|
|
86
|
+
$this->processPayment($order->total); // Server amount
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
CRITICAL PATTERN 2 — Negative values not validated
|
|
90
|
+
DETECT :
|
|
91
|
+
// Money transfer without sign validation
|
|
92
|
+
public function transfer(Request $request) {
|
|
93
|
+
$amount = $request->amount; // Can be negative!
|
|
94
|
+
|
|
95
|
+
auth()->user()->balance -= $amount;
|
|
96
|
+
$recipient->balance += $amount;
|
|
97
|
+
// If amount = -100 : the sender GAINS 100!
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Coupon application without validation
|
|
101
|
+
$discount = $coupon->discount_percentage / 100;
|
|
102
|
+
$total = $price - ($price * $discount);
|
|
103
|
+
// If discount = 150% : negative total → refund?
|
|
104
|
+
|
|
105
|
+
FIX :
|
|
106
|
+
public function transfer(Request $request) {
|
|
107
|
+
$request->validate([
|
|
108
|
+
'amount' => 'required|numeric|min:0.01|max:10000',
|
|
109
|
+
// min:0.01 prevents negative values and zero
|
|
110
|
+
]);
|
|
111
|
+
|
|
112
|
+
$amount = round((float)$request->amount, 2);
|
|
113
|
+
|
|
114
|
+
// Check balance before transaction
|
|
115
|
+
if (auth()->user()->balance < $amount) {
|
|
116
|
+
return response()->json(['error' => 'Insufficient balance'], 400);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
DB::transaction(function() use ($amount, $recipient) {
|
|
120
|
+
auth()->user()->decrement('balance', $amount);
|
|
121
|
+
$recipient->increment('balance', $amount);
|
|
122
|
+
|
|
123
|
+
// Transaction log for audit
|
|
124
|
+
Transaction::create([
|
|
125
|
+
'from_user_id' => auth()->id(),
|
|
126
|
+
'to_user_id' => $recipient->id,
|
|
127
|
+
'amount' => $amount,
|
|
128
|
+
'ip_address' => request()->ip(),
|
|
129
|
+
]);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
134
|
+
CATEGORY 2 — RACE CONDITIONS
|
|
135
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
136
|
+
|
|
137
|
+
PRINCIPLE :
|
|
138
|
+
Two simultaneous requests can both pass
|
|
139
|
+
the same check "before" the state changes.
|
|
140
|
+
Result : double spend, double action, double allocation.
|
|
141
|
+
|
|
142
|
+
CRITICAL PATTERN 3 — Double spend / Double action
|
|
143
|
+
DETECT :
|
|
144
|
+
// Code that checks THEN modifies without atomicity
|
|
145
|
+
public function usePromoCode(Request $request) {
|
|
146
|
+
$code = PromoCode::where('code', $request->code)
|
|
147
|
+
->where('used', false) // ← Check
|
|
148
|
+
->first();
|
|
149
|
+
|
|
150
|
+
if (!$code) {
|
|
151
|
+
return response()->json(['error' => 'Invalid code'], 400);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ← RACE CONDITION HERE
|
|
155
|
+
// Two simultaneous requests pass the check
|
|
156
|
+
// before either one sets used = true
|
|
157
|
+
|
|
158
|
+
$code->update(['used' => true]);
|
|
159
|
+
$this->applyDiscount(auth()->user());
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
EXPLOITATION SCENARIO :
|
|
163
|
+
Attacker sends 10 simultaneous requests.
|
|
164
|
+
All see used = false.
|
|
165
|
+
All apply the discount.
|
|
166
|
+
Code used 10 times instead of once.
|
|
167
|
+
|
|
168
|
+
FIX :
|
|
169
|
+
public function usePromoCode(Request $request) {
|
|
170
|
+
// Option A : Atomic update with where
|
|
171
|
+
$updated = PromoCode::where('code', $request->code)
|
|
172
|
+
->where('used', false)
|
|
173
|
+
->update([
|
|
174
|
+
'used' => true,
|
|
175
|
+
'used_by' => auth()->id(),
|
|
176
|
+
'used_at' => now(),
|
|
177
|
+
]);
|
|
178
|
+
|
|
179
|
+
// If no rows updated → code already used
|
|
180
|
+
if ($updated === 0) {
|
|
181
|
+
return response()->json(['error' => 'Invalid or already used code'], 400);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Option B : DB Lock (for more complex cases)
|
|
185
|
+
DB::transaction(function() use ($request) {
|
|
186
|
+
$code = PromoCode::where('code', $request->code)
|
|
187
|
+
->lockForUpdate() // SELECT ... FOR UPDATE
|
|
188
|
+
->first();
|
|
189
|
+
|
|
190
|
+
if (!$code || $code->used) {
|
|
191
|
+
throw new \Exception('Invalid code');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
$code->update(['used' => true, 'used_by' => auth()->id()]);
|
|
195
|
+
$this->applyDiscount(auth()->user());
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Option C : Redis for distributed operations
|
|
199
|
+
$key = "promo_code_lock:{$request->code}";
|
|
200
|
+
if (!Cache::add($key, true, 30)) { // NX = only if absent
|
|
201
|
+
return response()->json(['error' => 'Code already being used'], 429);
|
|
202
|
+
}
|
|
203
|
+
// Normal processing...
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
HIGH PATTERN 4 — Race condition on balance/stock
|
|
207
|
+
DETECT :
|
|
208
|
+
// Stock check without atomicity
|
|
209
|
+
if ($product->stock > 0) {
|
|
210
|
+
// ← Race condition possible
|
|
211
|
+
$product->decrement('stock');
|
|
212
|
+
// Stock can go negative with simultaneous requests
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
FIX :
|
|
216
|
+
// Atomic decrement with verification
|
|
217
|
+
$updated = Product::where('id', $product->id)
|
|
218
|
+
->where('stock', '>', 0)
|
|
219
|
+
->decrement('stock');
|
|
220
|
+
|
|
221
|
+
if ($updated === 0) {
|
|
222
|
+
return response()->json(['error' => 'Out of stock'], 400);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
226
|
+
CATEGORY 3 — WORKFLOW BYPASS
|
|
227
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
228
|
+
|
|
229
|
+
PRINCIPLE :
|
|
230
|
+
Process steps (order, verification,
|
|
231
|
+
approval) must be in order and verified.
|
|
232
|
+
Do not assume the previous step was done.
|
|
233
|
+
|
|
234
|
+
HIGH PATTERN 5 — Skipped workflow step
|
|
235
|
+
DETECT :
|
|
236
|
+
// Checkout without verifying cart is valid
|
|
237
|
+
public function confirmOrder(Request $request) {
|
|
238
|
+
$order = Order::findOrFail($request->order_id);
|
|
239
|
+
// No verification that payment was made!
|
|
240
|
+
// No verification of order status!
|
|
241
|
+
$order->update(['status' => 'confirmed']);
|
|
242
|
+
$this->shipOrder($order);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Publication without validation
|
|
246
|
+
public function publishArticle(Request $request) {
|
|
247
|
+
$article = Article::findOrFail($request->id);
|
|
248
|
+
// No verification article is in "draft" status
|
|
249
|
+
// Can a "rejected" article be republished directly?
|
|
250
|
+
$article->update(['status' => 'published']);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
FIX :
|
|
254
|
+
public function confirmOrder(Request $request) {
|
|
255
|
+
$order = Order::where('id', $request->order_id)
|
|
256
|
+
->where('user_id', auth()->id())
|
|
257
|
+
->firstOrFail();
|
|
258
|
+
|
|
259
|
+
// Explicit state machine
|
|
260
|
+
$allowedTransitions = [
|
|
261
|
+
'pending' => ['paid', 'cancelled'],
|
|
262
|
+
'paid' => ['confirmed', 'refunded'],
|
|
263
|
+
'confirmed' => ['shipped', 'cancelled'],
|
|
264
|
+
'shipped' => ['delivered'],
|
|
265
|
+
];
|
|
266
|
+
|
|
267
|
+
$targetStatus = 'confirmed';
|
|
268
|
+
$allowedNextStates = $allowedTransitions[$order->status] ?? [];
|
|
269
|
+
|
|
270
|
+
if (!in_array($targetStatus, $allowedNextStates)) {
|
|
271
|
+
return response()->json([
|
|
272
|
+
'error' => "Invalid transition: {$order->status} → {$targetStatus}"
|
|
273
|
+
], 422);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Check specific prerequisites
|
|
277
|
+
if ($targetStatus === 'confirmed' && !$order->payment_verified) {
|
|
278
|
+
return response()->json(['error' => 'Payment not verified'], 400);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
$order->update(['status' => $targetStatus]);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
285
|
+
CATEGORY 4 — FEATURE ABUSE
|
|
286
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
287
|
+
|
|
288
|
+
PRINCIPLE :
|
|
289
|
+
A legitimate feature can be abused
|
|
290
|
+
in a way not anticipated by the developer.
|
|
291
|
+
|
|
292
|
+
HIGH PATTERN 6 — Circular reference / self-reference
|
|
293
|
+
DETECT :
|
|
294
|
+
// Transfer to self
|
|
295
|
+
public function transfer(Request $request) {
|
|
296
|
+
// No check that from ≠ to
|
|
297
|
+
$sender = auth()->user();
|
|
298
|
+
$recipient = User::findOrFail($request->to_user_id);
|
|
299
|
+
|
|
300
|
+
// If sender = recipient : unexpected behavior?
|
|
301
|
+
// If calculation error : money created?
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Self-referral
|
|
305
|
+
public function applyReferral(Request $request) {
|
|
306
|
+
// No check that referrer ≠ referred
|
|
307
|
+
$referrer = User::where('code', $request->code)->first();
|
|
308
|
+
$this->applyBonus($referrer, auth()->user());
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
FIX :
|
|
312
|
+
public function transfer(Request $request) {
|
|
313
|
+
$request->validate([
|
|
314
|
+
'to_user_id' => 'required|integer|different:' . auth()->id(),
|
|
315
|
+
// different: prevents self-transfer
|
|
316
|
+
]);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
public function applyReferral(Request $request) {
|
|
320
|
+
$referrer = User::where('referral_code', $request->code)
|
|
321
|
+
->where('id', '!=', auth()->id()) // Not self
|
|
322
|
+
->first();
|
|
323
|
+
|
|
324
|
+
if (!$referrer) {
|
|
325
|
+
return response()->json(['error' => 'Invalid referral code'], 400);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Check they haven't already used a code
|
|
329
|
+
if (auth()->user()->referred_by !== null) {
|
|
330
|
+
return response()->json(['error' => 'Code already used'], 400);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
MEDIUM PATTERN 7 — Pagination/export abuse
|
|
335
|
+
DETECT :
|
|
336
|
+
// Export without limit
|
|
337
|
+
public function exportUsers(Request $request) {
|
|
338
|
+
// No limit → export of the ENTIRE database
|
|
339
|
+
return User::all()->toArray(); // Millions of records?
|
|
340
|
+
|
|
341
|
+
// Or pagination without limit
|
|
342
|
+
$limit = $request->limit ?? 100;
|
|
343
|
+
// Attacker sends limit=1000000
|
|
344
|
+
return User::paginate($limit);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
FIX :
|
|
348
|
+
public function exportUsers(Request $request) {
|
|
349
|
+
$request->validate([
|
|
350
|
+
'limit' => 'integer|max:1000', // Server-imposed maximum
|
|
351
|
+
]);
|
|
352
|
+
|
|
353
|
+
// Export by chunks with absolute limit
|
|
354
|
+
$maxExport = 10000;
|
|
355
|
+
$limit = min($request->limit ?? 100, $maxExport);
|
|
356
|
+
|
|
357
|
+
// Log the export (traceability)
|
|
358
|
+
AuditLog::create([
|
|
359
|
+
'action' => 'data_export',
|
|
360
|
+
'user_id' => auth()->id(),
|
|
361
|
+
'count' => $limit,
|
|
362
|
+
'ip' => request()->ip(),
|
|
363
|
+
]);
|
|
364
|
+
|
|
365
|
+
return User::select(['id', 'name', 'email']) // Minimal fields
|
|
366
|
+
->paginate($limit);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
370
|
+
CATEGORY 5 — TOCTOU (TIME OF CHECK / TIME OF USE)
|
|
371
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
372
|
+
|
|
373
|
+
PRINCIPLE :
|
|
374
|
+
The state verified at check time may have changed
|
|
375
|
+
by the time of use.
|
|
376
|
+
Check and use must be atomic.
|
|
377
|
+
|
|
378
|
+
HIGH PATTERN 8 — TOCTOU on eligibility
|
|
379
|
+
DETECT :
|
|
380
|
+
// Eligibility check followed by non-atomic action
|
|
381
|
+
public function claimReward(Request $request) {
|
|
382
|
+
$user = auth()->user();
|
|
383
|
+
|
|
384
|
+
// CHECK : Is the user eligible?
|
|
385
|
+
if ($user->points >= 1000 && !$user->reward_claimed) {
|
|
386
|
+
|
|
387
|
+
// ← TOCTOU HERE
|
|
388
|
+
// Between check and update,
|
|
389
|
+
// another request can modify state
|
|
390
|
+
|
|
391
|
+
sleep(1); // Simulate processing
|
|
392
|
+
|
|
393
|
+
// USE : Assign reward
|
|
394
|
+
$user->update([
|
|
395
|
+
'reward_claimed' => true,
|
|
396
|
+
'points' => $user->points - 1000
|
|
397
|
+
]);
|
|
398
|
+
|
|
399
|
+
$this->sendReward($user);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
FIX :
|
|
404
|
+
public function claimReward(Request $request) {
|
|
405
|
+
// Atomic check and update
|
|
406
|
+
$updated = User::where('id', auth()->id())
|
|
407
|
+
->where('points', '>=', 1000)
|
|
408
|
+
->where('reward_claimed', false)
|
|
409
|
+
->update([
|
|
410
|
+
'reward_claimed' => true,
|
|
411
|
+
'points' => DB::raw('points - 1000'),
|
|
412
|
+
'reward_date' => now(),
|
|
413
|
+
]);
|
|
414
|
+
|
|
415
|
+
if ($updated === 0) {
|
|
416
|
+
return response()->json([
|
|
417
|
+
'error' => 'Not eligible or reward already claimed'
|
|
418
|
+
], 400);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
$this->sendReward(auth()->user()->fresh());
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
425
|
+
CATEGORY 6 — UNVERIFIED LIMITS AND BOUNDARIES
|
|
426
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
427
|
+
|
|
428
|
+
MEDIUM PATTERN 9 — Business limits not enforced server-side
|
|
429
|
+
DETECT :
|
|
430
|
+
// Account limit per email not checked server-side
|
|
431
|
+
public function register(Request $request) {
|
|
432
|
+
// The "1 account per person" limit is only checked
|
|
433
|
+
// client-side → trivial bypass
|
|
434
|
+
User::create([...]);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Offer usage limit
|
|
438
|
+
public function claimOffer(Request $request) {
|
|
439
|
+
// "One offer per user" only verified
|
|
440
|
+
// by a UI constraint
|
|
441
|
+
Offer::create(['user_id' => auth()->id()]);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
FIX :
|
|
445
|
+
public function register(Request $request) {
|
|
446
|
+
// Server-side limit : 1 active account per email
|
|
447
|
+
$existingCount = User::where('email', $request->email)
|
|
448
|
+
->where('status', 'active')
|
|
449
|
+
->count();
|
|
450
|
+
|
|
451
|
+
if ($existingCount >= 1) {
|
|
452
|
+
return response()->json([
|
|
453
|
+
'error' => 'An account already exists with this email'
|
|
454
|
+
], 422);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// IP-based limit (anti-mass creation)
|
|
458
|
+
$recentRegistrations = User::where('registration_ip', $request->ip())
|
|
459
|
+
->where('created_at', '>', now()->subHour())
|
|
460
|
+
->count();
|
|
461
|
+
|
|
462
|
+
if ($recentRegistrations >= 3) {
|
|
463
|
+
return response()->json([
|
|
464
|
+
'error' => 'Too many accounts created from this address'
|
|
465
|
+
], 429);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
User::create([..., 'registration_ip' => $request->ip()]);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
472
|
+
CATEGORY 7 — STATE AND PARAMETER MANIPULATION
|
|
473
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
474
|
+
|
|
475
|
+
HIGH PATTERN 10 — Injectable context parameter
|
|
476
|
+
DETECT :
|
|
477
|
+
// Multi-tenant : tenant_id provided by client
|
|
478
|
+
public function getData(Request $request) {
|
|
479
|
+
$tenantId = $request->tenant_id; // ← Client controls!
|
|
480
|
+
|
|
481
|
+
$data = Data::where('tenant_id', $tenantId)->get();
|
|
482
|
+
// A user can access another tenant's data!
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Currency/locale manipulation
|
|
486
|
+
public function getPrice(Request $request) {
|
|
487
|
+
$currency = $request->currency ?? 'EUR';
|
|
488
|
+
$price = Product::find($id)->price;
|
|
489
|
+
return $this->convert($price, 'EUR', $currency);
|
|
490
|
+
// If conversion is poorly implemented → absurd prices
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
FIX :
|
|
494
|
+
public function getData(Request $request) {
|
|
495
|
+
// Always get tenant from session/token
|
|
496
|
+
// NEVER from the client request
|
|
497
|
+
$tenantId = auth()->user()->tenant_id; // From identity
|
|
498
|
+
|
|
499
|
+
$data = Data::where('tenant_id', $tenantId)->get();
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
public function getPrice(Request $request) {
|
|
503
|
+
// Validate currency against a whitelist
|
|
504
|
+
$allowedCurrencies = ['EUR', 'USD', 'GBP', 'JPY'];
|
|
505
|
+
$currency = in_array($request->currency, $allowedCurrencies)
|
|
506
|
+
? $request->currency
|
|
507
|
+
: 'EUR'; // Secure default
|
|
508
|
+
|
|
509
|
+
$price = Product::findOrFail($id)->price;
|
|
510
|
+
return $this->convert($price, 'EUR', $currency);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
═══════════════════════════════════════════════════════════════
|
|
514
|
+
DETECTION METHODOLOGY FOR THE AGENT
|
|
515
|
+
═══════════════════════════════════════════════════════════════
|
|
516
|
+
|
|
517
|
+
QUESTIONS TO ASK FOR EACH BUSINESS FEATURE
|
|
518
|
+
────────────────────────────────────────
|
|
519
|
+
|
|
520
|
+
□ Do monetary values/quantities come from the client?
|
|
521
|
+
→ Always recalculate server-side
|
|
522
|
+
|
|
523
|
+
□ Is there a "check then act" without atomicity?
|
|
524
|
+
→ Race condition risk → atomize with DB transaction
|
|
525
|
+
|
|
526
|
+
□ Are workflow steps verified sequentially?
|
|
527
|
+
→ Implement an explicit state machine
|
|
528
|
+
|
|
529
|
+
□ Can a feature be used on oneself?
|
|
530
|
+
→ Check self-references
|
|
531
|
+
|
|
532
|
+
□ Are business limits enforced server-side?
|
|
533
|
+
→ All business rules must be server-side
|
|
534
|
+
|
|
535
|
+
□ Does a context parameter (tenant, currency, role) come
|
|
536
|
+
from the client?
|
|
537
|
+
→ Always extract from token/session, never from body
|
|
538
|
+
|
|
539
|
+
□ What happens with extreme values?
|
|
540
|
+
(0, -1, MAX_INT, empty string, very long string)
|
|
541
|
+
→ Test edge cases
|
|
542
|
+
|
|
543
|
+
□ What happens if the action is repeated N times?
|
|
544
|
+
→ Idempotence and uniqueness
|
|
545
|
+
|
|
546
|
+
□ What happens if steps are done out of order?
|
|
547
|
+
→ Workflow enforcement
|
|
548
|
+
|
|
549
|
+
□ Who benefits financially or functionally
|
|
550
|
+
from exploiting this flaw?
|
|
551
|
+
→ Identify attacker motivation
|
|
552
|
+
|
|
553
|
+
═══════════════════════════════════════════════════════════════
|
|
554
|
+
BUSINESS LOGIC CHECKLIST
|
|
555
|
+
═══════════════════════════════════════════════════════════════
|
|
556
|
+
|
|
557
|
+
VALUES AND CALCULATIONS
|
|
558
|
+
□ Prices/amounts calculated server-side only?
|
|
559
|
+
□ Negative values rejected on quantities/amounts?
|
|
560
|
+
□ Zero values handled correctly?
|
|
561
|
+
□ Numeric overflow/underflow handled?
|
|
562
|
+
□ Consistent monetary rounding (no naive floating point)?
|
|
563
|
+
|
|
564
|
+
CONCURRENCY
|
|
565
|
+
□ Critical operations atomic (DB transactions)?
|
|
566
|
+
□ Locks used on shared resources?
|
|
567
|
+
□ Idempotence on replayable operations?
|
|
568
|
+
□ Double-submit prevention (forms, buttons)?
|
|
569
|
+
|
|
570
|
+
WORKFLOW
|
|
571
|
+
□ Explicit state machine for multi-step processes?
|
|
572
|
+
□ State transitions validated server-side?
|
|
573
|
+
□ Prerequisites for each step verified before execution?
|
|
574
|
+
□ Rollback on partial failure?
|
|
575
|
+
|
|
576
|
+
BUSINESS LIMITS
|
|
577
|
+
□ All "1 per user", "max N" rules server-side?
|
|
578
|
+
□ Self-references prohibited (self-transfer, self-referral)?
|
|
579
|
+
□ Context parameters (tenant, role) from session?
|
|
580
|
+
□ Export/pagination limits enforced server-side?
|
|
581
|
+
|
|
582
|
+
EDGE CASES
|
|
583
|
+
□ Behavior with banned/suspended user verified?
|
|
584
|
+
□ Behavior at end of offer period verified?
|
|
585
|
+
□ Behavior with zero stock verified?
|
|
586
|
+
□ Behavior at boundary timezones?
|
|
587
|
+
|
|
588
|
+
═══════════════════════════════════════════════════════════════
|
|
589
|
+
REFERENCES
|
|
590
|
+
═══════════════════════════════════════════════════════════════
|
|
591
|
+
|
|
592
|
+
OWASP Testing Guide — Business Logic Testing :
|
|
593
|
+
https://owasp.org/www-project-web-security-testing-guide/
|
|
594
|
+
latest/4-Web_Application_Security_Testing/10-Business_Logic_Testing/
|
|
595
|
+
|
|
596
|
+
CWE-840 — Business Logic Errors :
|
|
597
|
+
https://cwe.mitre.org/data/definitions/840.html
|
|
598
|
+
|
|
599
|
+
OWASP A04:2021 — Insecure Design :
|
|
600
|
+
https://owasp.org/Top10/A04_2021-Insecure_Design/
|
|
601
|
+
|
|
602
|
+
PortSwigger Business Logic Vulnerabilities :
|
|
603
|
+
https://portswigger.net/web-security/logic-flaws
|