-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy path08-comprehensive-protection.php
More file actions
380 lines (308 loc) · 13.6 KB
/
Copy path08-comprehensive-protection.php
File metadata and controls
380 lines (308 loc) · 13.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
<?php
/**
* Example 08: Comprehensive Production-Ready Protection
*
* This example combines all protection strategies into a production-ready
* configuration that defends against multiple attack vectors:
*
* - Brute Force (Fail2Ban + throttling)
* - DDoS/Rate Abuse (tiered rate limiting)
* - Scanner Detection (User-Agent + path blocking)
* - Path Traversal (pattern detection)
*
* OWASP CRS detection (SQL injection, XSS, ...) is available via the companion
* package flowd/phirewall-preset-owasp-crs (see Layer 3 below).
*
* Run: php examples/08-comprehensive-protection.php
*/
declare(strict_types=1);
require __DIR__ . '/../vendor/autoload.php';
use Flowd\Phirewall\Config;
use Flowd\Phirewall\Config\DiagnosticsCounters;
use Flowd\Phirewall\Config\DiagnosticsDispatcher;
use Flowd\Phirewall\Http\TrustedProxyResolver;
use Flowd\Phirewall\KeyExtractors;
use Flowd\Phirewall\Middleware;
use Flowd\Phirewall\Store\InMemoryCache;
use Nyholm\Psr7\Factory\Psr17Factory;
use Nyholm\Psr7\Response;
use Nyholm\Psr7\ServerRequest;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
echo "=== Comprehensive Production-Ready Protection ===\n\n";
// =============================================================================
// CONFIGURATION
// =============================================================================
$cache = new InMemoryCache();
$diagnostics = new DiagnosticsCounters();
$config = new Config($cache, new DiagnosticsDispatcher($diagnostics));
$config->setKeyPrefix('prod');
$config->enableRateLimitHeaders();
// Trusted proxy configuration (adjust for your infrastructure)
$proxyResolver = new TrustedProxyResolver([
'127.0.0.1',
'10.0.0.0/8',
'172.16.0.0/12',
'192.168.0.0/16',
]);
// Make the proxy-resolved client IP the default for every rule: keyless counter
// rules, IP matchers, keyIp() and filterIp() all key on the real client from here on.
$config->setIpResolver($proxyResolver->resolve(...));
echo "Configuration:\n";
echo " Key prefix: prod\n";
echo " Rate limit headers: enabled\n\n";
// =============================================================================
// LAYER 1: SAFELISTS (Bypass all checks)
// =============================================================================
echo "Layer 1: Safelists\n";
$config->safelists->add('health', fn($req): bool => $req->getUri()->getPath() === '/health');
echo " - /health endpoint\n";
$config->safelists->add('metrics', fn($req): bool => $req->getUri()->getPath() === '/metrics');
echo " - /metrics endpoint\n";
$config->safelists->add('internal-ips', function ($req) use ($proxyResolver): bool {
$ip = $proxyResolver->resolve($req);
if ($ip === null) {
return false;
}
// Allow localhost and specific internal range
return $ip === '127.0.0.1' || str_starts_with($ip, '192.168.10.');
});
echo " - Internal IP range (192.168.10.0/24)\n\n";
// =============================================================================
// LAYER 2: BLOCKLISTS (Immediate denial)
// =============================================================================
echo "Layer 2: Blocklists\n";
// Scanner User-Agents
$scanners = ['sqlmap', 'nikto', 'nmap', 'burp', 'dirbuster', 'wfuzz', 'nuclei'];
$config->blocklists->add('scanner-ua', function ($req) use ($scanners): bool {
$ua = strtolower((string) $req->getHeaderLine('User-Agent'));
foreach ($scanners as $scanner) {
if (str_contains($ua, $scanner)) {
return true;
}
}
return false;
});
echo " - Scanner User-Agents (" . count($scanners) . " patterns)\n";
// Scanner paths
$blockedPaths = ['/.svn', '/phpmyadmin', '/.env', '/phpinfo.php'];
$config->blocklists->add('scanner-paths', function ($req) use ($blockedPaths): bool {
$path = strtolower((string) $req->getUri()->getPath());
// Match the .git directory itself, not paths that merely start with it (e.g. /.github).
if (preg_match('#/\.git($|/)#', $path) === 1) {
return true;
}
foreach ($blockedPaths as $blockedPath) {
if (str_starts_with($path, $blockedPath)) {
return true;
}
}
return false;
});
echo " - Scanner paths (" . (count($blockedPaths) + 1) . " patterns)\n";
// Path traversal
$config->blocklists->add('path-traversal', function ($req): bool {
$input = urldecode($req->getUri()->getPath() . '?' . $req->getUri()->getQuery());
return preg_match('~\.\.[\\\\/]~', $input) === 1;
});
echo " - Path traversal patterns\n\n";
// =============================================================================
// LAYER 3: OWASP CRS RULES (SQL Injection, XSS, etc.)
// =============================================================================
//
// OWASP CRS detection lives in the companion package
// flowd/phirewall-preset-owasp-crs, which ships the SecRule engine plus
// ready-made blocklist/fail2ban presets:
//
// use Flowd\PhirewallPresetOwaspCrs\{ParanoiaLevel, Presets};
// $config = $config->with(Presets::blocklist(ParanoiaLevel::Level1));
//
// =============================================================================
echo "Layer 3: OWASP CRS - optional, install flowd/phirewall-preset-owasp-crs to enable\n";
// =============================================================================
// LAYER 4: FAIL2BAN (Brute force protection)
// =============================================================================
echo "Layer 4: Fail2Ban & Allow2Ban\n";
// Fail2Ban: an upstream WAF flags a request as malicious. Fail2Ban blocks every
// such match on sight (403) and bans the source after a few within the window.
$config->fail2ban->add('waf-flagged',
threshold: 3,
period: 60,
ban: 3600,
filter: fn($req): bool => $req->getHeaderLine('X-Threat') === '1',
);
echo " - WAF-flagged: blocked on sight, banned after 3 in 1min\n";
// Allow2Ban with a filter: count login attempts and ban after 5. An attempt
// is still a legitimate request, so matching attempts pass until the threshold.
$config->allow2ban->add('login-brute',
threshold: 5,
period: 300,
banSeconds: 3600,
key: fn($req): string => $req->getServerParams()['REMOTE_ADDR'] ?? '',
filter: fn($req): bool => $req->getMethod() === 'POST' && $req->getUri()->getPath() === '/login',
);
echo " - Login: 5 attempts in 5min = 1 hour ban\n\n";
// =============================================================================
// LAYER 5: THROTTLING (Rate limiting)
// =============================================================================
echo "Layer 5: Throttling\n";
// Global limit
$config->throttles->add('global', limit: 200, period: 60);
echo " - Global: 200/min per IP\n";
// Write operations
$config->throttles->add('write-ops', limit: 50, period: 60, key: function ($req) use ($proxyResolver): ?string {
if (in_array($req->getMethod(), ['POST', 'PUT', 'PATCH', 'DELETE'])) {
return $proxyResolver->resolve($req);
}
return null;
});
echo " - Write ops: 50/min per IP\n";
// Login endpoint
$config->throttles->add('login', limit: 10, period: 60, key: function ($req) use ($proxyResolver): ?string {
if ($req->getUri()->getPath() === '/login') {
return $proxyResolver->resolve($req);
}
return null;
});
echo " - Login: 10/min per IP\n";
// Authenticated users (higher limit)
$config->throttles->add('user', limit: 1000, period: 60, key: KeyExtractors::header('X-User-Id'));
echo " - Authenticated: 1000/min per user\n";
// Burst detection
$config->throttles->add('burst', limit: 30, period: 5);
echo " - Burst: 30/5s per IP\n\n";
// =============================================================================
// CUSTOM RESPONSES
// =============================================================================
$config->blocklistedResponseFactory = new \Flowd\Phirewall\Config\Response\ClosureBlocklistedResponseFactory(
fn(string $rule, string $type, $req): ResponseInterface => new Response(403, ['Content-Type' => 'application/json'], json_encode([
'error' => 'Forbidden',
'message' => 'Your request has been blocked by our security system.',
'code' => 'SECURITY_BLOCK',
], JSON_THROW_ON_ERROR))
);
$config->throttledResponseFactory = new \Flowd\Phirewall\Config\Response\ClosureThrottledResponseFactory(
fn(string $rule, int $retryAfter, $req): ResponseInterface => new Response(429, ['Content-Type' => 'application/json'], json_encode([
'error' => 'Too Many Requests',
'message' => 'Rate limit exceeded. Please try again later.',
'retry_after' => $retryAfter,
], JSON_THROW_ON_ERROR))
);
// =============================================================================
// SIMULATION
// =============================================================================
$middleware = new Middleware($config, new Psr17Factory());
$handler = new class implements RequestHandlerInterface {
public function handle(ServerRequestInterface $serverRequest): ResponseInterface
{
return new Response(200, ['Content-Type' => 'application/json'], '{"status":"ok"}');
}
};
$test = function (string $desc, string $method, string $path, array $headers = [], string $ip = '203.0.113.1') use ($middleware, $handler): int {
$headers = array_merge(['User-Agent' => 'Mozilla/5.0'], $headers);
$request = new ServerRequest($method, $path, $headers, null, '1.1', ['REMOTE_ADDR' => $ip]);
$response = $middleware->process($request, $handler);
return $response->getStatusCode();
};
echo "=== Attack Simulation ===\n\n";
// Test matrix
$tests = [
['Safe request', 'GET', '/api/users', [], '203.0.113.1', 200],
['Health check (safelisted)', 'GET', '/health', [], '203.0.113.1', 200],
// Scanners
['SQLMap scanner', 'GET', '/api', ['User-Agent' => 'sqlmap/1.7'], '1.1.1.1', 403],
['Nikto scanner', 'GET', '/', ['User-Agent' => 'Nikto/2.5'], '1.1.1.2', 403],
// Scanner paths
['Repo leak probe', 'GET', '/.svn/entries', [], '1.1.1.3', 403],
['.env access', 'GET', '/.env', [], '1.1.1.4', 403],
// SQL injection and XSS detection are not built into core; they live in the
// OWASP CRS companion package (flowd/phirewall-preset-owasp-crs, see Layer 3),
// so this core-only example does not assert on them.
// Path traversal
['Path traversal', 'GET', '/files/../../../etc/passwd', [], '1.1.1.9', 403],
];
$passed = 0;
$failed = 0;
foreach ($tests as [$desc, $method, $path, $headers, $ip, $expected]) {
$actual = $test($desc, $method, $path, $headers, $ip);
$status = $actual === $expected ? 'PASS' : 'FAIL';
if ($actual === $expected) {
++$passed;
} else {
++$failed;
}
echo sprintf("[%s] %-25s => %d (expected %d)\n", $status, $desc, $actual, $expected);
}
echo "\n";
// Fail2Ban test: every WAF-flagged request is blocked on sight; the 3rd bans.
echo "Fail2Ban Test (WAF-flagged requests, blocked on sight):\n";
$threatIp = '198.51.100.9';
for ($i = 1; $i <= 4; ++$i) {
$status = $test('Flagged request ' . $i, 'GET', '/', ['X-Threat' => '1'], $threatIp);
$desc = $i < 3 ? '(blocked match)' : '(banned)';
echo sprintf(" Request %d %s: %d\n", $i, $desc, $status);
}
echo "\n";
// Allow2Ban test: login attempts pass until the 5th, which bans the IP.
echo "Allow2Ban Test (simulating login brute force):\n";
$bruteForceIp = '198.51.100.1';
for ($i = 1; $i <= 7; ++$i) {
// First 5 requests are login attempts (filter matches), then 2 page views
$path = $i <= 5 ? '/login' : '/';
$method = $i <= 5 ? 'POST' : 'GET';
$status = $test('Login attempt ' . $i, $method, $path, [], $bruteForceIp);
$desc = $i < 5 ? '(attempt, allowed)' : '(banned)';
echo sprintf(" Attempt %d %s: %d\n", $i, $desc, $status);
}
echo "\n";
// Rate limiting test
echo "Rate Limiting Test (30 rapid requests from single IP):\n";
$ip = '10.20.30.40';
$blocked = 0;
for ($i = 1; $i <= 35; ++$i) {
$status = $test('Request ' . $i, 'GET', '/api/data', [], $ip);
if ($status === 429) {
++$blocked;
}
if ($i <= 3 || $i >= 30) {
echo sprintf(" Request %d: %d\n", $i, $status);
} elseif ($i === 4) {
echo " ... (requests 4-29) ...\n";
}
}
echo " Blocked by rate limit: {$blocked} requests\n\n";
// =============================================================================
// RESULTS
// =============================================================================
echo "=== Results ===\n";
echo sprintf('Attack tests passed: %d%s', $passed, PHP_EOL);
echo "Attack tests failed: {$failed}\n\n";
echo "=== Diagnostics Summary ===\n";
$counters = $diagnostics->all();
$categories = [
'safelisted' => 'Safelisted (bypassed)',
'blocklisted' => 'Blocked by blocklist',
'throttle_exceeded' => 'Throttled (rate limited)',
'fail2ban_matched' => 'Blocked by Fail2Ban (match)',
'fail2ban_banned' => 'Banned by Fail2Ban',
'allow2ban_banned' => 'Banned by Allow2Ban',
'passed' => 'Passed (allowed)',
];
foreach ($categories as $key => $label) {
$total = $counters[$key]['total'] ?? 0;
echo sprintf(" %-30s: %d\n", $label, $total);
// Show breakdown by rule
foreach ($counters[$key]['by_rule'] ?? [] as $rule => $count) {
echo sprintf(" - %-25s: %d\n", $rule, $count);
}
}
echo "\n=== Production Deployment Checklist ===\n";
echo "[ ] Replace InMemoryCache with RedisCache for multi-server\n";
echo "[ ] Configure TrustedProxyResolver with your load balancer IPs\n";
echo "[ ] Adjust rate limits based on expected traffic\n";
echo "[ ] Enable OWASP diagnostics header only in staging\n";
echo "[ ] Set up event dispatcher for logging/alerting\n";
echo "[ ] Test with your application's specific endpoints\n";
echo "[ ] Monitor diagnostics counters in production\n";
echo "\n=== Example Complete ===\n";