-
Notifications
You must be signed in to change notification settings - Fork 0
Adjust OWASP docs for version 0.5 #20
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
8c41fd5
Add a performance page with per-scenario engine costs
sascha-egerer 7efa1a4
Document CRS anomaly scoring, target exclusions, manipulators and mat…
sascha-egerer fd3b80c
Fix bundled-snapshot rule ids and log-context key list on the CRS page
sascha-egerer 176b1fd
Document the configurable per-value inspection cap and correct the @r…
sascha-egerer f83cd03
Document conditional exclusions, CRS exclusion syntax and the 0.5 mat…
sascha-egerer 4b497cc
Canonicalize operator casing and fix the severity-fallback wording
sascha-egerer 214b2d9
Document the dead-exclusion-tag warning
sascha-egerer 2149d91
Document the request parameter of exclusion conditions
sascha-egerer 9088feb
Smooth the exclusion-tag tip wording
sascha-egerer de493f9
Align the exclusion-condition and manipulator callback signatures
sascha-egerer File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,141 @@ | ||
| # Performance | ||
|
|
||
| Phirewall runs on every request, so its cost matters. This page shows what a | ||
| `decide()` call costs in isolation, what that means under load, and what is | ||
| left of it inside a real application. The short version: a typical rule set | ||
| costs single-digit **microseconds** per request, OWASP CRS costs a | ||
| **millisecond or two**, and a blocked request is cheaper than a served one. | ||
|
|
||
| ## Engine cost per request | ||
|
|
||
| Measured with the engine alone: 20,000 `decide()` calls per scenario after | ||
| warmup, `InMemoryCache` store, PHP 8.5 with OPcache and PCRE JIT, Apple | ||
| M-series laptop. The rule set is the Quick Start shape: one safelist, the | ||
| known-scanners blocklist, one path blocklist, one fail2ban, one allow2ban, | ||
| one throttle. | ||
|
|
||
| | Scenario | Outcome | Cost per request | Requests/sec (one core) | | ||
| |----------|---------|-----------------:|------------------------:| | ||
| | No rules at all (engine floor) | pass | 1.6 us | 640,000 | | ||
| | 6 rules, nothing matches (normal traffic) | pass | 8.1 us | 123,000 | | ||
| | 6 rules, safelist match (early exit) | safelisted | 1.4 us | 740,000 | | ||
| | 6 rules, blocklist match: scanner UA | blocked (403) | 1.9 us | 520,000 | | ||
| | 6 rules, blocklist match: path | blocked (403) | 3.2 us | 316,000 | | ||
| | 6 rules, fail2ban match below threshold | blocked (403) | 7.8 us | 129,000 | | ||
| | 6 rules, key already banned | blocked (403) | 5.2 us | 193,000 | | ||
| | 6 rules, throttle exceeded | throttled (429) | 8.7 us | 115,000 | | ||
|
|
||
| Two things stand out: | ||
|
|
||
| - **The expensive case is normal traffic**, because a request that matches | ||
| nothing is checked against every rule. Matches exit early and are cheaper. | ||
| - **Attackers are the cheap case.** A blocklist match costs 2 to 3 us, and a | ||
| banned key is decided by a single store lookup. The requests you block never | ||
| reach your application, so under attack the firewall saves far more time | ||
| than it costs. | ||
|
|
||
| Absolute numbers depend on your hardware and PHP build. Reproduce them on | ||
| your own machine with the bundled benchmarks: | ||
|
|
||
| ```bash | ||
| XDEBUG_MODE=off php examples/20-rule-benchmarks.php # rule scenarios | ||
| XDEBUG_MODE=off php examples/13-benchmarks.php # store backends | ||
| ``` | ||
|
|
||
| ## Store cost | ||
|
|
||
| The table above uses the in-process `InMemoryCache`. The counter rules | ||
| (fail2ban, allow2ban, throttle, track) are the ones that talk to the | ||
| [store](/features/storage): every request pays one ban lookup per ban-capable | ||
| rule, and requests a counter rule matches additionally pay a counter write. | ||
| The same scenarios per store, measured inside a Docker setup where Redis and | ||
| MariaDB run as separate containers: | ||
|
|
||
| | Scenario (6 rules) | InMemory | APCu | Redis | PDO/MySQL | | ||
| |--------------------|---------:|-----:|------:|----------:| | ||
| | Nothing matches (2 ban lookups) | 6 us | 6 us | 111 us | 110 us | | ||
| | Throttle counting a request | 8 us | 8 us | 213 us | 667 us | | ||
| | fail2ban match (403, counter write) | 5 us | 6 us | 160 us | 661 us | | ||
| | Key already banned (403) | 4 us | 4 us | 52 us | 55 us | | ||
|
|
||
| How to read it: APCu behaves like in-memory - shared counters on one server | ||
| at no measurable cost. The network stores pay roughly 50 us per round trip, | ||
| so reads (ban lookups, the every-request cost) sit around 0.1 ms for both. | ||
| Writes separate them: a Redis counter update is one round trip (~0.2 ms), | ||
| MySQL pays transaction and durability overhead (~0.7 ms). Even the worst | ||
| case - every request writing a counter through PDO/MySQL - stays under one | ||
| millisecond. Since bans decide on the read path, an attack wave hits the | ||
| cheap lookup, not the write. | ||
|
|
||
| ## OWASP CRS cost | ||
|
|
||
| The [CRS preset](/features/owasp-crs) evaluates a few hundred ModSecurity | ||
| rules per request, most of them regular expressions over your request | ||
| parameters, so its cost scales with the number and size of arguments: | ||
|
|
||
| | Request shape (paranoia level 1, no match) | Cost per request | | ||
| |--------------------------------------------|-----------------:| | ||
| | GET without parameters | ~0.1 ms | | ||
| | GET with 3 query parameters | ~1.0 ms | | ||
| | POST with 8 form fields | ~2.4 ms | | ||
| | SQL injection attempt (matches, early exit) | ~0.4 ms | | ||
|
|
||
| Parsing the rule files costs a few milliseconds once; with the | ||
| [compiled data cache](/features/owasp-crs#caching) that price is paid on the | ||
| first request after a deployment, not on every request. Combining CRS with a | ||
| fail2ban preset also pays off here: once a probing client is banned, the CRS | ||
| engine no longer runs for it - the request is rejected by the cheap | ||
| banned-key lookup from the table above. | ||
|
|
||
| ## What that means under load | ||
|
|
||
| Cost per request times requests per second gives the CPU share the firewall | ||
| needs. For the two typical setups: | ||
|
|
||
| | Sustained load | 6 rules (~8 us) | CRS PL1 (~1 ms) | | ||
| |----------------|----------------:|----------------:| | ||
| | 10 req/s | 0.008% of one core | 1% of one core | | ||
| | 100 req/s | 0.08% of one core | 10% of one core | | ||
| | 1,000 req/s | 0.8% of one core | one full core | | ||
|
|
||
| Rule-of-thumb: counting rules are free at any realistic load; give CRS a | ||
| thought once you serve hundreds of uncached requests per second, and scope it | ||
| to the routes that need it if you do. | ||
|
|
||
| ## Inside a real application | ||
|
|
||
| Measured end to end on a TYPO3 site (Docker, Apache + mod_php 8.4, cached | ||
| page, median of 400 requests per run), once per store: | ||
|
|
||
| | Scenario | APCu | Redis | PDO/MySQL | | ||
| |----------|-----:|------:|----------:| | ||
| | Firewall middleware removed | 9.1 ms | | | | ||
| | Firewall active, 6 rules, nothing matches | 9.6 ms | 9.9 ms | 10.0 ms | | ||
| | Blocklist match (scanner user agent, 403) | 6.7 ms | | | | ||
| | Banned key (403) | 5.4 ms | 6.4 ms | 5.9 ms | | ||
|
|
||
| The no-match overhead on a cached page is well under a millisecond, and the | ||
| store differences drown in run-to-run noise - even with every ban lookup | ||
| going through MySQL. On uncached pages the overhead disappears entirely next | ||
| to rendering time. And the blocked cases respond *faster* than the page | ||
| itself: the request is stopped in the middleware before routing, rendering, | ||
| or content queries run. | ||
|
|
||
| ## Keeping it fast | ||
|
|
||
| - **OPcache on** (it is on virtually every production setup): the firewall is | ||
| plain PHP and profits like the rest of your code. | ||
| - **PCRE JIT on** (`pcre.jit=1`, the PHP default): every regex filter and the | ||
| CRS `@rx` operators rely on it. See | ||
| [PCRE JIT](/features/owasp-crs#pcre-jit). | ||
| - **Compiled data cache** for preset packages, so CRS and bad-IP data are not | ||
| re-parsed per request. See | ||
| [Caching expensive preset data](/advanced/presets#caching-expensive-preset-data). | ||
| - **Pick the store deliberately**: APCu for a single server (as fast as | ||
| in-memory), Redis when counters and bans must be shared (~0.1 ms of ban | ||
| lookups per request, ~0.2 ms per counter write). PDO/MySQL works without | ||
| extra infrastructure and stays under a millisecond even on counter writes, | ||
| but it puts firewall traffic on your database. | ||
| - **Scope expensive rules.** A throttle with a `scope` filter skips its | ||
| counter work for requests outside the scope, and CRS can be registered for | ||
| the routes that actually take user input. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.