{"id":1441,"date":"2025-11-06T21:08:22","date_gmt":"2025-11-06T18:08:22","guid":{"rendered":"https:\/\/www.dchost.com\/blog\/the-calm-waf-how-i-tune-modsecurity-owasp-crs-to-cut-false-positives-and-keep-sites-fast\/"},"modified":"2025-11-06T21:08:22","modified_gmt":"2025-11-06T18:08:22","slug":"the-calm-waf-how-i-tune-modsecurity-owasp-crs-to-cut-false-positives-and-keep-sites-fast","status":"publish","type":"post","link":"https:\/\/www.dchost.com\/blog\/en\/the-calm-waf-how-i-tune-modsecurity-owasp-crs-to-cut-false-positives-and-keep-sites-fast\/","title":{"rendered":"The Calm WAF: How I Tune ModSecurity + OWASP CRS to Cut False Positives and Keep Sites Fast"},"content":{"rendered":"<div class=\"dchost-blog-content-wrapper\"><p>It started with a late-night ping from a friend: \u201cOur checkout is throwing weird errors, but only for some users. We didn\u2019t change anything.\u201d You know that stomach drop when you realize the Web Application Firewall is probably doing its job a little too enthusiastically? I\u2019ve been there more times than I\u2019d like to admit. The logs are full of scary-looking alerts, the devs are frustrated, the marketing team is waving a launch deadline, and all you want is a WAF that stops the bad stuff without stepping on legitimate traffic.<\/p>\n<p>Here\u2019s the thing: ModSecurity with the OWASP Core Rule Set (CRS) can be a lifesaver, but it needs a little love. Out of the box, it\u2019s like a guard dog that barks at every unfamiliar noise. With some thoughtful tuning, though, it becomes calm, confident, and accurate. In this post, I\u2019ll walk you through how I approach false positive reduction and performance tuning for ModSecurity + CRS, with stories from real-world rollouts, the why behind each tweak, and the settings that have earned their spot in my go-to template.<\/p>\n<p>We\u2019ll talk about a workflow that catches issues early, a safe way to roll out rules, practical examples of exclusions that don\u2019t turn your firewall into a screen door, and how to keep request inspection snappy. By the end, you\u2019ll have a playbook you can pick up tomorrow morning and apply to your own stack.<\/p>\n<div id=\"toc_container\" class=\"toc_transparent no_bullets\"><p class=\"toc_title\">\u0130&ccedil;indekiler<\/p><ul class=\"toc_list\"><li><a href=\"#What_I_Aim_For_When_I_Deploy_ModSecurity_CRS\"><span class=\"toc_number toc_depth_1\">1<\/span> What I Aim For When I Deploy ModSecurity + CRS<\/a><\/li><li><a href=\"#A_Friendly_Crash_Course_ModSecurity_CRS_and_Why_False_Positives_Happen\"><span class=\"toc_number toc_depth_1\">2<\/span> A Friendly Crash Course: ModSecurity, CRS, and Why False Positives Happen<\/a><\/li><li><a href=\"#My_Rollout_Workflow_Quiet_First_Then_Confident\"><span class=\"toc_number toc_depth_1\">3<\/span> My Rollout Workflow: Quiet First, Then Confident<\/a><ul><li><a href=\"#Step_1_Detection_only_with_anomaly_scoring\"><span class=\"toc_number toc_depth_2\">3.1<\/span> Step 1: Detection only, with anomaly scoring<\/a><\/li><li><a href=\"#Step_2_Set_a_blocking_threshold_and_monitor\"><span class=\"toc_number toc_depth_2\">3.2<\/span> Step 2: Set a blocking threshold and monitor<\/a><\/li><li><a href=\"#Step_3_Nudge_the_paranoia_level_gradually\"><span class=\"toc_number toc_depth_2\">3.3<\/span> Step 3: Nudge the paranoia level gradually<\/a><\/li><\/ul><\/li><li><a href=\"#False_Positive_Tuning_A_Playbook_That_Doesnt_Weaken_Your_Shield\"><span class=\"toc_number toc_depth_1\">4<\/span> False Positive Tuning: A Playbook That Doesn\u2019t Weaken Your Shield<\/a><ul><li><a href=\"#1_Build_a_short_feedback_loop\"><span class=\"toc_number toc_depth_2\">4.1<\/span> 1) Build a short feedback loop<\/a><\/li><li><a href=\"#2_Exclude_by_surgical_path_or_parameter\"><span class=\"toc_number toc_depth_2\">4.2<\/span> 2) Exclude by surgical path or parameter<\/a><\/li><li><a href=\"#3_Whitelist_admin-only_routescarefully\"><span class=\"toc_number toc_depth_2\">4.3<\/span> 3) Whitelist admin-only routes\u2014carefully<\/a><\/li><li><a href=\"#4_Tackle_common_hot_spots_SQLi_and_XSS_alerts_on_legit_inputs\"><span class=\"toc_number toc_depth_2\">4.4<\/span> 4) Tackle common hot spots: SQLi and XSS alerts on legit inputs<\/a><\/li><li><a href=\"#5_Be_kind_to_uploads\"><span class=\"toc_number toc_depth_2\">4.5<\/span> 5) Be kind to uploads<\/a><\/li><li><a href=\"#6_Dont_forget_outbound_rules\"><span class=\"toc_number toc_depth_2\">4.6<\/span> 6) Don\u2019t forget outbound rules<\/a><\/li><\/ul><\/li><li><a href=\"#Performance_Tuning_Make_It_Fast_Without_Losing_Sleep\"><span class=\"toc_number toc_depth_1\">5<\/span> Performance Tuning: Make It Fast Without Losing Sleep<\/a><ul><li><a href=\"#Skip_what_doesnt_need_scanning\"><span class=\"toc_number toc_depth_2\">5.1<\/span> Skip what doesn\u2019t need scanning<\/a><\/li><li><a href=\"#Right-size_your_PCRE_and_logging\"><span class=\"toc_number toc_depth_2\">5.2<\/span> Right-size your PCRE and logging<\/a><\/li><li><a href=\"#Tune_request_phases_and_transformations\"><span class=\"toc_number toc_depth_2\">5.3<\/span> Tune request phases and transformations<\/a><\/li><li><a href=\"#Scope_your_configuration_to_your_traffic\"><span class=\"toc_number toc_depth_2\">5.4<\/span> Scope your configuration to your traffic<\/a><\/li><\/ul><\/li><li><a href=\"#A_Real-World_Tuning_Story_The_Checkout_That_Kept_Tripping_XSS\"><span class=\"toc_number toc_depth_1\">6<\/span> A Real-World Tuning Story: The Checkout That Kept Tripping XSS<\/a><\/li><li><a href=\"#Your_First_90_Minutes_A_Practical_Checklist\"><span class=\"toc_number toc_depth_1\">7<\/span> Your First 90 Minutes: A Practical Checklist<\/a><ul><li><a href=\"#1_Start_quiet\"><span class=\"toc_number toc_depth_2\">7.1<\/span> 1) Start quiet<\/a><\/li><li><a href=\"#2_Turn_on_anomaly_scoring_dashboards\"><span class=\"toc_number toc_depth_2\">7.2<\/span> 2) Turn on anomaly scoring dashboards<\/a><\/li><li><a href=\"#3_Set_thresholds_and_move_to_blocking\"><span class=\"toc_number toc_depth_2\">7.3<\/span> 3) Set thresholds and move to blocking<\/a><\/li><li><a href=\"#4_Try_paranoia_level_2_gently\"><span class=\"toc_number toc_depth_2\">7.4<\/span> 4) Try paranoia level 2, gently<\/a><\/li><li><a href=\"#5_Trim_the_fat_for_performance\"><span class=\"toc_number toc_depth_2\">7.5<\/span> 5) Trim the fat for performance<\/a><\/li><\/ul><\/li><li><a href=\"#Examples_You_Can_Copy-Paste_Adapt_With_Care\"><span class=\"toc_number toc_depth_1\">8<\/span> Examples You Can Copy-Paste (Adapt With Care)<\/a><ul><li><a href=\"#Exclude_static_assets_from_WAF_inspection\"><span class=\"toc_number toc_depth_2\">8.1<\/span> Exclude static assets from WAF inspection<\/a><\/li><li><a href=\"#Remove_one_noisy_rule_for_one_parameter_on_one_path\"><span class=\"toc_number toc_depth_2\">8.2<\/span> Remove one noisy rule for one parameter on one path<\/a><\/li><li><a href=\"#Allow_larger_file_uploads_without_blowing_memory\"><span class=\"toc_number toc_depth_2\">8.3<\/span> Allow larger file uploads without blowing memory<\/a><\/li><li><a href=\"#Switch_from_detection-only_to_blocking_with_thresholds\"><span class=\"toc_number toc_depth_2\">8.4<\/span> Switch from detection-only to blocking with thresholds<\/a><\/li><\/ul><\/li><li><a href=\"#How_I_Think_About_Safety_Nets_Dont_Tune_in_a_Vacuum\"><span class=\"toc_number toc_depth_1\">9<\/span> How I Think About Safety Nets: Don\u2019t Tune in a Vacuum<\/a><\/li><li><a href=\"#Troubleshooting_Reading_the_Audit_Log_Without_Losing_Your_Mind\"><span class=\"toc_number toc_depth_1\">10<\/span> Troubleshooting: Reading the Audit Log Without Losing Your Mind<\/a><\/li><li><a href=\"#Useful_Docs_and_Where_I_Look_Things_Up\"><span class=\"toc_number toc_depth_1\">11<\/span> Useful Docs and Where I Look Things Up<\/a><\/li><li><a href=\"#Wrap-Up_Keep_the_WAF_Calm_and_Itll_Keep_You_Calm\"><span class=\"toc_number toc_depth_1\">12<\/span> Wrap-Up: Keep the WAF Calm, and It\u2019ll Keep You Calm<\/a><\/li><\/ul><\/div>\n<h2 id=\"section-1\"><span id=\"What_I_Aim_For_When_I_Deploy_ModSecurity_CRS\">What I Aim For When I Deploy ModSecurity + CRS<\/span><\/h2>\n<p>Whenever I add a WAF to a site, I set two goals. First, stop actual bad requests\u2014SQL injection payloads, XSS attempts, protocol anomalies, and bot noise. Second, don\u2019t break normal users or admin workflows. Sounds obvious. But in practice, that balance takes a bit of craftsmanship.<\/p>\n<p>My mindset is simple: treat the WAF like a safety net layered under good app hygiene and smart edge rules. It\u2019s not your only defense, and it shouldn\u2019t feel like handcuffs during a release. That\u2019s why I never slam a WAF into full block mode on day one. I stage it, I learn from its alerts, and then I dial in the rules until \u201csilent, accurate, fast\u201d becomes the default state.<\/p>\n<p>If you like layered defense talk, I\u2019ve shared a bigger picture of that approach here: <a href=\"https:\/\/www.dchost.com\/blog\/en\/waf-ve-bot-korumasi-cloudflare-modsecurity-ve-fail2bani-ayni-masada-baristirmanin-sicacik-hikayesi\/\">the layered shield I trust with Cloudflare, ModSecurity, and Fail2ban<\/a>. Think of this article as the deep-dive on the ModSecurity + CRS part of that shield.<\/p>\n<h2 id=\"section-2\"><span id=\"A_Friendly_Crash_Course_ModSecurity_CRS_and_Why_False_Positives_Happen\">A Friendly Crash Course: ModSecurity, CRS, and Why False Positives Happen<\/span><\/h2>\n<p>Let\u2019s set the scene with a quick, conversational overview. ModSecurity is the engine\u2014the part that sits in your web server and inspects requests and responses. The OWASP Core Rule Set (CRS) is the brain\u2014the curated set of signatures, behaviors, and anomaly checks that tells ModSecurity what to look for. You can run ModSecurity with Apache, Nginx, or even as part of some proxies. It\u2019s flexible, which is one reason I keep coming back to it.<\/p>\n<p>CRS has personality. It looks not only for obvious attacks but also for suspicious patterns across headers, URLs, bodies, and encodings. That thoroughness is why you\u2019ll see false positives: real-world apps are messy. Someone\u2019s API uses JSON fields called \u201cselect\u201d or \u201cunion.\u201d Your search box accepts angle brackets because it powers an internal template editor. A plugin sends encoded payloads that look exactly like what a WAF would block in a different context. None of this means CRS is wrong\u2014it just means your app deserves some custom fit.<\/p>\n<p>If you haven\u2019t browsed the docs, they\u2019re straightforward and worth a skim: the <a href=\"https:\/\/coreruleset.org\/docs\/\" rel=\"nofollow noopener\" target=\"_blank\">OWASP Core Rule Set documentation<\/a> lays out the rule groups, paranoia levels, and tuning methods in plain language. I\u2019ll distill the bits I actually touch in production below.<\/p>\n<h2 id=\"section-3\"><span id=\"My_Rollout_Workflow_Quiet_First_Then_Confident\">My Rollout Workflow: Quiet First, Then Confident<\/span><\/h2>\n<h3><span id=\"Step_1_Detection_only_with_anomaly_scoring\">Step 1: Detection only, with anomaly scoring<\/span><\/h3>\n<p>When I first deploy CRS on a live site, I start in \u201cDetectionOnly\u201d mode. In CRS 3.x, anomaly scoring is the default strategy. Each suspicious pattern adds points. A typical flow is: let the WAF score requests silently, ship logs, and watch what bubbles up. The goal is to learn the app\u2019s shape without blocking users on day one.<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\"># Global engine in detection-only\nSecRuleEngine DetectionOnly\n\n# CRS core includes (example path; adapt to your distro)\nInclude \/etc\/modsecurity\/crs\/crs-setup.conf\nInclude \/etc\/modsecurity\/crs\/rules\/*.conf\n<\/code><\/pre>\n<p>Why anomaly scoring? It\u2019s gentler than single-rule blocking. Users rarely trigger multiple independent rule groups unless they\u2019re doing something sketchy. That makes it easier to separate noisy patterns from truly risky requests.<\/p>\n<h3><span id=\"Step_2_Set_a_blocking_threshold_and_monitor\">Step 2: Set a blocking threshold and monitor<\/span><\/h3>\n<p>After a few days, I enable blocking with a threshold that fits the app\u2019s behavior. For many apps, a threshold of 5 is a decent starting point. If you\u2019re seeing borderline cases in the logs, you can go a notch higher and still catch the majority of real attacks. Then we iterate.<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\"># Switch to blocking mode once you trust your telemetry\nSecRuleEngine On\n\n# In crs-setup.conf, set anomaly thresholds (example values)\n# tx.inbound_anomaly_score_threshold=5\n# tx.outbound_anomaly_score_threshold=4\n<\/code><\/pre>\n<p>Don\u2019t rush this phase. Those first weeks teach you a ton about the app\u2019s rough edges. I like to schedule quick \u201cWAF check-ins\u201d with the dev team to review logs and spot patterns early, before someone reports a broken workflow.<\/p>\n<h3><span id=\"Step_3_Nudge_the_paranoia_level_gradually\">Step 3: Nudge the paranoia level gradually<\/span><\/h3>\n<p>CRS gives you \u201cparanoia levels,\u201d essentially sensitivity presets. Level 1 is friendly and broadly compatible; level 2 adds depth; higher levels get very strict. My approach: start at 1, fix the low-hanging false positives, then try level 2 in detection only for a period. If it\u2019s quiet, promote it to block mode. Rinse and repeat. The official <a href=\"https:\/\/coreruleset.org\/docs\/concepts\/paranoia_levels\/\" rel=\"nofollow noopener\" target=\"_blank\">CRS paranoia levels guide<\/a> explains what each level adds, and it matches what I see in the real world.<\/p>\n<h2 id=\"section-4\"><span id=\"False_Positive_Tuning_A_Playbook_That_Doesnt_Weaken_Your_Shield\">False Positive Tuning: A Playbook That Doesn\u2019t Weaken Your Shield<\/span><\/h2>\n<p>I learned the hard way that quick fixes\u2014like turning off huge chunks of rules\u2014feel good in the moment and hurt later. The trick is to be precise. Think in terms of scope: what path, what parameter, what header, and in what context? Here\u2019s how I keep the shield strong while removing the friction.<\/p>\n<h3><span id=\"1_Build_a_short_feedback_loop\">1) Build a short feedback loop<\/span><\/h3>\n<p>When an alert lands, I ask three questions: is this user flow legitimate, is the matched pattern actually risky, and can I narrow the context so the rule still catches real attacks elsewhere? I scan the ModSecurity audit log for the Rule ID, the matched variable (ARGS, REQUEST_HEADERS, REQUEST_COOKIES), and the exact snippet flagged. That tells me where to aim the exclusion.<\/p>\n<h3><span id=\"2_Exclude_by_surgical_path_or_parameter\">2) Exclude by surgical path or parameter<\/span><\/h3>\n<p>Let\u2019s say a REST endpoint legitimately accepts JSON fields like \u201corder[select]\u201d or raw HTML. Instead of disabling an entire rule group globally, I\u2019ll either remove a specific rule for that location or remove a specific variable (target) from inspection. For example, excluding a single parameter from a single rule that\u2019s noisy:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\"># Example: For a specific path, remove ARGS:comment from a rule (e.g., XSS rule 941100)\n# Adapt syntax to Apache\/Nginx context blocks as needed.\n\nSecRule REQUEST_URI &quot;^\/api\/review$&quot; &quot;id:10010,phase:1,pass,t:none,ctl:ruleRemoveTargetById=941100;ARGS:comment&quot;\n<\/code><\/pre>\n<p>If a whole endpoint is special (like a WYSIWYG admin path), I\u2019ll limit exclusions to that path only. That way, the rest of the app continues to enjoy full protection.<\/p>\n<h3><span id=\"3_Whitelist_admin-only_routescarefully\">3) Whitelist admin-only routes\u2014carefully<\/span><\/h3>\n<p>Admin dashboards often include features\u2014file uploads, embedded HTML, complex filters\u2014that can trigger CRS. If the route is behind authentication and additional checks, I\u2019m okay with lighter inspection there. But I don\u2019t go straight to \u201cWAF off.\u201d I might remove high-noise rules and keep protocol and anomaly checks in place. If you absolutely must disable inspection, scope it tightly:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\"># Apache-style example for a very specific admin uploader path\n&lt;LocationMatch &quot;\/admin\/uploader&quot;&gt;\n    SecRuleEngine Off\n&lt;\/LocationMatch&gt;\n<\/code><\/pre>\n<p>In Nginx with the ModSecurity connector, use a location block and turn modsecurity off for that route. The goal is to be intentional and explicit. Future you will thank you.<\/p>\n<h3><span id=\"4_Tackle_common_hot_spots_SQLi_and_XSS_alerts_on_legit_inputs\">4) Tackle common hot spots: SQLi and XSS alerts on legit inputs<\/span><\/h3>\n<p>If you\u2019ve run CRS for more than a week, you\u2019ve seen this: an input that contains words like \u201cselect\u201d or \u201cunion\u201d or a search box using \u201c\u201d for templates. My approach is: first, check the request to confirm the context really needs those strings. Second, prefer removing only the relevant rule(s) for that path or parameter. For example, a review comment field that allows angle brackets could trigger 941100 (XSS). I\u2019ll narrow that rule\u2019s target to exclude just ARGS:comment on the review endpoint, as shown earlier.<\/p>\n<p>On SQLi, pay attention to content type. A lot of false positives happen when JSON bodies contain short tokens that match SQL keywords. If the endpoint is internal or authenticated, and the pattern is expected, I\u2019ll remove SQLi checks just for that parameter or endpoint, not globally.<\/p>\n<h3><span id=\"5_Be_kind_to_uploads\">5) Be kind to uploads<\/span><\/h3>\n<p>Large file uploads are an easy way to both slow down your WAF and create confusion. I usually keep a higher body limit for multipart forms while capping in-memory buffering and making sure timeouts are sane. Here\u2019s a baseline I\u2019ve used often:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\"># Body limits (tune to your infra)\nSecRequestBodyLimit 13107200            # 12.5MB\nSecRequestBodyNoFilesLimit 131072       # 128KB for non-file bodies\nSecRequestBodyInMemoryLimit 131072      # Keep memory usage moderate\nSecResponseBodyAccess Off               # Usually off unless you need response inspection\n<\/code><\/pre>\n<p>Uploads will generate fewer false positives if you treat them differently and confirm allowed MIME types at the app layer. CRS does its best, but the app is the most reliable bouncer at the door for what files should be accepted.<\/p>\n<h3><span id=\"6_Dont_forget_outbound_rules\">6) Don\u2019t forget outbound rules<\/span><\/h3>\n<p>CRS can also inspect responses. It\u2019s not as commonly used, but it can catch server leaks or unintended data. For most sites, I keep response inspection minimal for performance reasons. If you enable it, start in detection only and keep your thresholds conservative.<\/p>\n<h2 id=\"section-5\"><span id=\"Performance_Tuning_Make_It_Fast_Without_Losing_Sleep\">Performance Tuning: Make It Fast Without Losing Sleep<\/span><\/h2>\n<p>I used to think WAF slowness was inevitable. It isn\u2019t. Most of the perceived slowness comes from a few hotspots: inspecting static files, overly broad rules applied to traffic that doesn\u2019t need them, and debug-heavy logging. Trim those, and ModSecurity hums along quietly.<\/p>\n<h3><span id=\"Skip_what_doesnt_need_scanning\">Skip what doesn\u2019t need scanning<\/span><\/h3>\n<p>There\u2019s no reason to inspect every PNG, CSS, or JS as if it were a risky POST request. I exclude static assets and health checks from deep inspection. In Apache, I\u2019ll wrap those paths and switch off the engine. In Nginx, I\u2019ll set modsecurity off in the locations that serve static files.<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\"># Apache-style example for static paths\n&lt;LocationMatch &quot;.*.(?:css|js|png|jpg|jpeg|gif|ico|svg)$&quot;&gt;\n    SecRuleEngine Off\n&lt;\/LocationMatch&gt;\n<\/code><\/pre>\n<p>Even when the engine is on, make sure your rule set and request phases aren\u2019t wasting time on traffic that can\u2019t possibly be malicious.<\/p>\n<h3><span id=\"Right-size_your_PCRE_and_logging\">Right-size your PCRE and logging<\/span><\/h3>\n<p>CRS uses a lot of regular expressions. Enabling PCRE JIT at build time can help on busy systems. Also, set reasonable PCRE match limits to prevent pathological cases from causing CPU spikes. And in production, keep debug logging low. It\u2019s helpful in a pinch but expensive if left on.<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\"># Good production defaults (tune as needed)\nSecDebugLogLevel 0\nSecAuditLogRelevantStatus &quot;@lt 400&quot;\nSecPcreMatchLimit 100000\nSecPcreMatchLimitRecursion 100000\n<\/code><\/pre>\n<p>I once found a staging server crawling because debug logging was left at a high level. The fix was as glamorous as flipping a switch. Don\u2019t overlook the boring wins.<\/p>\n<h3><span id=\"Tune_request_phases_and_transformations\">Tune request phases and transformations<\/span><\/h3>\n<p>CRS rules run in phases (think \u201cearly request checks\u201d vs \u201cpost-body checks\u201d). Most of the heavy lifting happens after the request body is available. If your app almost never uses request bodies on certain paths, you can short-circuit deep inspection there. Likewise, transformations (like URL decoding or lowercasing) are essential for catching obfuscated attacks, but you can remove a target from a specific rule if the cost outweighs the benefit in a narrow context.<\/p>\n<h3><span id=\"Scope_your_configuration_to_your_traffic\">Scope your configuration to your traffic<\/span><\/h3>\n<p>If your app is almost entirely JSON APIs, treat it like that. If it\u2019s mostly static with a handful of dynamic endpoints, embrace that shape. The same WAF config for every path is the slowest and least accurate. The more you reflect your app\u2019s reality in your rules, the less work ModSecurity has to do.<\/p>\n<h2 id=\"section-6\"><span id=\"A_Real-World_Tuning_Story_The_Checkout_That_Kept_Tripping_XSS\">A Real-World Tuning Story: The Checkout That Kept Tripping XSS<\/span><\/h2>\n<p>A client\u2019s checkout was fine under test but sporadically broke in production. We dug into the audit logs and found a pattern: a loyalty field was triggering an XSS rule because some users pasted coupon codes containing template-like strings. The rule wasn\u2019t wrong. The input could have been risky. But we had several layers of server-side output encoding, and the field wasn\u2019t rendered back to users without escaping.<\/p>\n<p>We made a narrow exclusion: remove that specific field from the specific XSS rule on the checkout endpoint, and only for authenticated sessions. Then we set an alert for any multi-rule anomaly on that endpoint. False positive gone, protection still in place. Performance also improved because the app wasn\u2019t retrying failed steps.<\/p>\n<p>The punchline is that the most effective WAF tuning tends to be boring: small, scoped, and logged. Save the dramatic changes for last resort.<\/p>\n<h2 id=\"section-7\"><span id=\"Your_First_90_Minutes_A_Practical_Checklist\">Your First 90 Minutes: A Practical Checklist<\/span><\/h2>\n<h3><span id=\"1_Start_quiet\">1) Start quiet<\/span><\/h3>\n<p>Switch ModSecurity to DetectionOnly. Enable CRS. Keep your current deployment paths untouched. Let it run for a couple of days to collect data without impacting users.<\/p>\n<h3><span id=\"2_Turn_on_anomaly_scoring_dashboards\">2) Turn on anomaly scoring dashboards<\/span><\/h3>\n<p>Point the audit log to a place where you can query it. I like to track top Rule IDs, affected paths, and client IP segments. A few hot paths will show up quickly. Fix those first.<\/p>\n<h3><span id=\"3_Set_thresholds_and_move_to_blocking\">3) Set thresholds and move to blocking<\/span><\/h3>\n<p>Pick a threshold (5 is a good middle ground), enable blocking, and continue to monitor. Create small, focused exclusions for known-good flows. If you\u2019re nervous, start with blocking on staging and detection-only on production, then flip the switch once you\u2019re comfortable.<\/p>\n<h3><span id=\"4_Try_paranoia_level_2_gently\">4) Try paranoia level 2, gently<\/span><\/h3>\n<p>Crank CRS to level 2 in detection-only. Let it sit for a week. If it\u2019s quiet, enable it in blocking mode. You\u2019ll be pleasantly surprised how much extra coverage you gain with a handful of surgical exclusions.<\/p>\n<h3><span id=\"5_Trim_the_fat_for_performance\">5) Trim the fat for performance<\/span><\/h3>\n<p>Exclude static assets from scanning. Re-check your debug log levels. Make sure PCRE limits are set. Confirm body limits match your app\u2019s upload habits. Each of these takes minutes and pays off every day.<\/p>\n<h2 id=\"section-8\"><span id=\"Examples_You_Can_Copy-Paste_Adapt_With_Care\">Examples You Can Copy-Paste (Adapt With Care)<\/span><\/h2>\n<h3><span id=\"Exclude_static_assets_from_WAF_inspection\">Exclude static assets from WAF inspection<\/span><\/h3>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\"># Apache example\n&lt;LocationMatch &quot;.*.(?:css|js|png|jpg|jpeg|gif|ico|svg|woff2?)$&quot;&gt;\n    SecRuleEngine Off\n&lt;\/LocationMatch&gt;\n<\/code><\/pre>\n<h3><span id=\"Remove_one_noisy_rule_for_one_parameter_on_one_path\">Remove one noisy rule for one parameter on one path<\/span><\/h3>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\"># Targeted exclusion for XSS rule on a comment field\nSecRule REQUEST_URI &quot;^\/api\/review$&quot; &quot;id:10010,phase:1,pass,t:none,ctl:ruleRemoveTargetById=941100;ARGS:comment&quot;\n<\/code><\/pre>\n<h3><span id=\"Allow_larger_file_uploads_without_blowing_memory\">Allow larger file uploads without blowing memory<\/span><\/h3>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">SecRequestBodyLimit 13107200\nSecRequestBodyInMemoryLimit 131072\nSecRequestBodyNoFilesLimit 131072\n<\/code><\/pre>\n<h3><span id=\"Switch_from_detection-only_to_blocking_with_thresholds\">Switch from detection-only to blocking with thresholds<\/span><\/h3>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\"># Engine On, anomaly thresholds set in crs-setup.conf\nSecRuleEngine On\n# In crs-setup.conf:\n# setvar:tx.inbound_anomaly_score_threshold=5\n# setvar:tx.outbound_anomaly_score_threshold=4\n<\/code><\/pre>\n<h2 id=\"section-9\"><span id=\"How_I_Think_About_Safety_Nets_Dont_Tune_in_a_Vacuum\">How I Think About Safety Nets: Don\u2019t Tune in a Vacuum<\/span><\/h2>\n<p>WAFs do their best work as part of a broader plan. If you\u2019re already using an edge provider with basic bot rules and rate limiting, let that layer absorb some junk traffic. Your origin WAF then deals with the trickier stuff that slips through. Likewise, if your app sanitizes output correctly and validates inputs thoroughly, you can afford to be more forgiving in certain endpoints without opening doors you\u2019ll regret.<\/p>\n<p>The layers don\u2019t compete\u2014they relax each other. That\u2019s the quiet magic of a balanced stack. Your pages feel fast, your logs stay readable, and your team only hears about security when it matters.<\/p>\n<h2 id=\"section-10\"><span id=\"Troubleshooting_Reading_the_Audit_Log_Without_Losing_Your_Mind\">Troubleshooting: Reading the Audit Log Without Losing Your Mind<\/span><\/h2>\n<p>Audit logs can feel like a wall of text at first. Here\u2019s how I skim efficiently. I look for the section with the matched Rule ID and the \u201cMatched Data\u201d snippet. That usually points me straight to the parameter or header that tripped the rule. Next, I scan the rule tag\u2014XSS, SQLi, protocol anomaly\u2014to understand the family. Then, I check the request path and method. If it\u2019s a POST to a JSON endpoint, I already have a mental shortlist of typical false positives.<\/p>\n<p>When I\u2019m ready to write an exclusion, I copy the path pattern, note the parameter name, and decide between \u201cremove this rule for this path\u201d or \u201cremove this rule\u2019s target for this parameter.\u201d I save global rule removals for the rare cases where a rule simply doesn\u2019t fit the app at all. And I document every change with the Rule ID and why we did it. Future merges go smoother when your config tells a story.<\/p>\n<h2 id=\"section-11\"><span id=\"Useful_Docs_and_Where_I_Look_Things_Up\">Useful Docs and Where I Look Things Up<\/span><\/h2>\n<p>When I get stuck or want to double-check syntax, I go straight to the official docs. The <a href=\"https:\/\/coreruleset.org\/docs\/\" rel=\"nofollow noopener\" target=\"_blank\">OWASP Core Rule Set documentation<\/a> is my home base for rule behavior and concepts. For a deeper dive into the engine itself, the <a href=\"https:\/\/github.com\/SpiderLabs\/ModSecurity\/wiki\/Reference-Manual\" rel=\"nofollow noopener\" target=\"_blank\">ModSecurity reference manual<\/a> covers directives, variables, and gotchas. And if you\u2019re experimenting with stricter policies, the <a href=\"https:\/\/coreruleset.org\/docs\/concepts\/paranoia_levels\/\" rel=\"nofollow noopener\" target=\"_blank\">CRS paranoia levels guide<\/a> helps you set expectations before you flip the switch.<\/p>\n<h2 id=\"section-12\"><span id=\"Wrap-Up_Keep_the_WAF_Calm_and_Itll_Keep_You_Calm\">Wrap-Up: Keep the WAF Calm, and It\u2019ll Keep You Calm<\/span><\/h2>\n<p>I remember the first time I rolled out CRS without a plan. It felt like tossing a net into a storm and hoping for the best. Since then, the playbook has become almost meditative: start quiet, learn the app\u2019s voice, tighten the rules, and stay specific with exclusions. The result is a WAF that rarely surprises you, a team that trusts it, and users who never know it\u2019s there.<\/p>\n<p>If you\u2019re about to start tuning, keep it simple. Set anomaly thresholds that feel comfortable. Trim static paths from inspection. Tackle the loudest false positives with one-parameter, one-path exclusions. Slowly elevate paranoia levels once the noise dies down. And revisit your settings when the app changes\u2014new features deserve a quick \u201cWAF warm-up\u201d just like they deserve tests and monitoring.<\/p>\n<p>Hope this was helpful! If you want more on the bigger security picture, check out the layered approach I mentioned earlier. And if you found a tweak that made your WAF both calmer and faster, I\u2019d genuinely love to hear about it. See you in the next post, and may your logs be boring in the best possible way.<\/p>\n<\/div>","protected":false},"excerpt":{"rendered":"<p>It started with a late-night ping from a friend: \u201cOur checkout is throwing weird errors, but only for some users. We didn\u2019t change anything.\u201d You know that stomach drop when you realize the Web Application Firewall is probably doing its job a little too enthusiastically? I\u2019ve been there more times than I\u2019d like to admit. [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":1442,"comment_status":"","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[26],"tags":[],"class_list":["post-1441","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-teknoloji"],"_links":{"self":[{"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/posts\/1441","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/comments?post=1441"}],"version-history":[{"count":0,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/posts\/1441\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/media\/1442"}],"wp:attachment":[{"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/media?parent=1441"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/categories?post=1441"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/tags?post=1441"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}