Nothing ruins a calm on-call shift like a CEO screenshot of a WordPress â403 Forbiddenâ on the login page. The marketing team canât publish. Editors canât upload. Customers canât check out. And your WAF, which is supposed to be the quiet adult in the room, is now the bouncer throwing out the guests.
The fix is not âdisable the WAFâ or âwhitelist everything under /wp-admin/â. The fix is to get evidence, isolate the exact rule, and tune the smallest possible exception with guardrails. You want WordPress working and attackers bored. Both are achievable.
Whatâs actually happening when WordPress is âblocked by the WAFâ
When users say âthe WAF blocked WordPress,â they usually mean one of four things:
- A true positive: someone (or something) is actually attempting SQLi/XSS/RCE and the WAF blocked it. WordPress is just where the traffic landed.
- A false positive: legitimate WordPress behavior looks suspicious. The usual suspects: editor HTML, REST API requests, AJAX endpoints, file uploads, or plugin admin pages with odd parameters.
- A rate/behavior control trigger: bot protection, login protection, or generic request rate limiting is tripping on admin activity, cron jobs, or a CDN health check.
- A mismatch of trust boundaries: the app sees different client IPs than the WAF sees, or cookies/headers are altered by a proxy. A rule meant to protect one layer is now punishing another.
Most WordPress/WAF pain comes from endpoint concentration. WordPress funnels a lot of complex behavior through a few URLs:
/wp-login.phpfor auth/wp-admin/admin-ajax.phpfor asynchronous UI behavior (frontend and backend)/wp-json/for the REST API (including Gutenberg/editor features)/xmlrpc.phphistorically used for remote publishing; now mostly used by bots and old integrations/wp-admin/for everything else that looks âadminnyâ and therefore âattackyâ
WAFs donât âunderstand WordPress.â They understand HTTP requests and patterns that often correlate with attacks. WordPress generates content and parameters that accidentally resemble attacks. Your job is to narrow the mismatch without removing meaningful protection.
One guiding idea, paraphrased from John Allspaw: operations is about enabling change without getting hurt. That applies to WAFs tooâsafe exceptions are a form of operational maturity.
Fast diagnosis playbook (first/second/third)
First: confirm itâs the WAF (not the app, not the CDN, not auth)
- Look for HTTP status: 403/406/429 are common WAF outcomes, but apps also return 403.
- Check for WAF headers (vendor-specific) and a request ID you can trace.
- Correlate timestamp and client IP across CDN/proxy and origin logs.
Second: identify the exact rule and the exact request component
- Pull the WAF event/audit log entry: rule ID/group, matched variable (ARGS, REQUEST_URI, REQUEST_HEADERS), matched payload snippet.
- Determine whether it matched on URI, query parameter, body, cookie, or header.
- Confirm whether the rule is in block mode or log/count mode.
Third: choose the least-dangerous tuning lever
- Fix the app/request if itâs legitimately weird (bad plugin, broken encoding, huge cookies).
- Scope down using URI + method + authentication state (admin only) before you consider disabling a rule.
- Exclude the smallest variable (a single parameter) rather than an entire rule group.
- Raise anomaly thresholds only when you can prove the traffic is safe and you have compensating controls.
Interesting facts and context (because history repeats)
- ModSecurity started life in the early 2000s as an Apache moduleâWAFs were originally a âhot patchâ for vulnerable apps, not a silver bullet.
- The OWASP Core Rule Set (CRS) became the default âgenericâ WAF brain for many stacks; itâs designed to be broad, not WordPress-specific.
- WordPressâs XML-RPC endpoint was a big deal in the late 2000s for remote publishing; today itâs commonly abused for brute force and amplification patterns.
- Gutenberg (the block editor) shifted WordPress toward heavier REST API usage; many âsuddenâ WAF issues appeared after editor upgrades because request shapes changed.
- Early WAF deployments often ran in âdetect onlyâ mode for weeks; blocking immediately caused outages. Mature teams still stage WAF changes like code.
- âPositive securityâ models (allow known-good patterns) predate modern WAF marketing; theyâre harder to run, but more robust for admin endpoints.
- Some SQLi detection signatures are decades old; attackers adapt, but so do applicationsâfalse positives often come from modern JSON bodies tripping legacy patterns.
- CDNs popularized managed WAF rules and made âWAF in front of everythingâ common; the upside is scale, the downside is youâre now debugging a distributed policy engine.
Principles for safe WAF tuning (no security theater)
1) Donât negotiate with 403s; collect evidence
Every WAF product has a way to tell you which rule fired and what it matched. If you canât see that, youâre operating blind. When stakeholders demand an instant fix, give them an instant triage: âI can stop the bleeding by bypassing WAF for this path, but I need 30 minutes to do it safely.â Then actually do it safely.
2) Exceptions should be narrower than your patience
Good exceptions are scoped by:
- URI path (
/wp-json/vs âall of WordPressâ) - HTTP method (allow
GETbut scrutinizePOST) - Authentication context (admin cookie present, or requests from VPN, or from a known IdP-protected admin)
- Parameter name (exclude
contentfield, not the entire body) - Rule ID (disable one rule in one location, not an entire managed ruleset)
3) Prefer âfix the triggerâ over âdisable the detectorâ
If a plugin sends a parameter that looks like SQLi because it includes select and union as plain words, the right fix may be: encode properly, change field names, reduce reflection, or sanitize content earlier. WAF tuning is not a replacement for application hygiene.
4) Make room for boring controls
WordPress benefits enormously from boring, reliable mitigations: rate limiting on login, bot filtering, MFA, least-privileged admin accounts, blocking unused endpoints, and keeping plugins updated. WAF tuning is less scary when you have compensating controls.
Joke #1: Disabling the WAF to fix WordPress is like removing smoke detectors because you donât like the beeping. It works until it doesnât.
Hands-on tasks: commands, outputs, and decisions
Below are practical tasks you can run on a typical Linux origin with Nginx/Apache and ModSecurity, plus a few CDN/WAF-adjacent checks. Each task includes: command, what the output means, and what decision you make.
Task 1: Reproduce the block with a known request and capture headers
cr0x@server:~$ curl -kisS https://example.com/wp-login.php | sed -n '1,20p'
HTTP/2 403
date: Fri, 27 Dec 2025 10:14:22 GMT
content-type: text/html; charset=UTF-8
server: cloudflare
cf-ray: 86a1c2d3e4f56789-FRA
What it means: HTTP 403 with a Cloudflare header suggests block at the edge WAF, not your origin app. The cf-ray value is your trace handle.
Decision: Pull the WAF event using the request ID (or timestamp+IP) before touching origin rules.
Task 2: Check origin response directly (bypass CDN) to prove where the block happens
cr0x@server:~$ curl -kisS --resolve example.com:443:203.0.113.10 https://example.com/wp-login.php | sed -n '1,20p'
HTTP/2 200
date: Fri, 27 Dec 2025 10:15:01 GMT
content-type: text/html; charset=UTF-8
server: nginx
What it means: Origin returns 200, edge returns 403. The WAF/CDN is blocking, not WordPress.
Decision: Tune edge WAF rules. Do not waste time in PHP logs yet.
Task 3: Identify which endpoint is blocked (admin, REST, AJAX, uploads)
cr0x@server:~$ curl -kisS https://example.com/wp-json/wp/v2/users/me | sed -n '1,15p'
HTTP/2 403
date: Fri, 27 Dec 2025 10:16:11 GMT
content-type: application/json
server: cloudflare
What it means: The REST API is blocked. That can break Gutenberg, media, and plugin UIs.
Decision: Look for managed rules targeting JSON bodies, auth headers, or generic âAPI attackâ signatures; tune per /wp-json/ not globally.
Task 4: If ModSecurity is on origin, confirm itâs enabled and in which mode
cr0x@server:~$ sudo nginx -T 2>/dev/null | grep -E 'modsecurity|SecRuleEngine' -n
121:modsecurity on;
122:modsecurity_rules_file /etc/nginx/modsec/main.conf;
What it means: Nginx is compiled with ModSecurity connector and itâs enabled.
Decision: Next step: check whether ModSecurity is in DetectionOnly or On and inspect audit logs.
Task 5: Check SecRuleEngine setting
cr0x@server:~$ sudo grep -R --line-number 'SecRuleEngine' /etc/nginx/modsec
/etc/nginx/modsec/main.conf:12:SecRuleEngine On
What it means: Requests can be actively blocked on origin.
Decision: If youâre mid-incident, consider temporary switch to DetectionOnly for a specific location while you analyze (not a blanket off switch).
Task 6: Locate ModSecurity audit log and confirm itâs receiving events
cr0x@server:~$ sudo grep -R --line-number 'SecAuditLog' /etc/nginx/modsec
/etc/nginx/modsec/main.conf:45:SecAuditLog /var/log/modsec_audit.log
cr0x@server:~$ sudo tail -n 3 /var/log/modsec_audit.log
--c9a1b7d8-H--
Message: Access denied with code 403 (phase 2). Matched "Operator `Rx' with parameter `(?i:(?:union(?:.*)select))'" against variable `ARGS:query' (Value: `union select`) [file "/etc/modsecurity.d/owasp-crs/rules/REQUEST-942-APPLICATION-ATTACK-SQLI.conf"] [id "942100"] [severity "CRITICAL"] [tag "application-multi"] [hostname "example.com"] [uri "/wp-admin/admin-ajax.php"] [unique_id "1735294592"]
What it means: Rule 942100 (SQLi category) fired on ARGS:query in admin-ajax.php. Thatâs specific enough to tune precisely.
Decision: Do not disable all SQLi rules. Create a targeted exclusion for parameter query on that endpoint if you validate itâs safe and expected.
Task 7: Correlate the request in access logs (method, size, user agent, client IP)
cr0x@server:~$ sudo awk '$7 ~ /admin-ajax\.php/ {print $1,$4,$6,$7,$9,$10,$12}' /var/log/nginx/access.log | tail -n 5
198.51.100.23 [27/Dec/2025:10:16:31 +0000] "POST /wp-admin/admin-ajax.php 403 721 "Mozilla/5.0"
198.51.100.23 [27/Dec/2025:10:16:34 +0000] "POST /wp-admin/admin-ajax.php 403 721 "Mozilla/5.0"
What it means: Repeated 403s for POST to admin-ajax. The client IP is stable; looks like a real browser.
Decision: If the same block hits many real users across IPs, itâs likely a false positive. If itâs one IP hammering, it may be an attackerâdonât tune, block.
Task 8: Extract matched parameter values from audit log to see whatâs triggering
cr0x@server:~$ sudo grep -n 'Matched "Operator' -n /var/log/modsec_audit.log | tail -n 2
4128:Message: Access denied with code 403 (phase 2). Matched "Operator `Rx' with parameter `(?i:(?:union(?:.*)select))'" against variable `ARGS:query' (Value: `union select`) [id "942100"] [uri "/wp-admin/admin-ajax.php"] [unique_id "1735294592"]
What it means: The literal value union select is present. That could be a malicious payload, or it could be a search/filter UI that allows SQL keywords (bad idea, but it happens).
Decision: Validate the feature generating it. If itâs a legitimate search syntax, you need compensating controls (auth-only, nonce checks, rate limiting) before excluding.
Task 9: Confirm WordPress nonce/auth context for admin-ajax calls
cr0x@server:~$ curl -kisS -X POST https://example.com/wp-admin/admin-ajax.php \
-d 'action=test_action&_ajax_nonce=deadbeef&query=union%20select' | sed -n '1,25p'
HTTP/2 403
date: Fri, 27 Dec 2025 10:18:12 GMT
content-type: text/html; charset=UTF-8
What it means: Still blocked at WAF; you canât even reach WordPressâs nonce validation.
Decision: If you plan an exception for admin-ajax, scope it to authenticated admin sessions (where possible) or to specific actions/parameters rather than blanket admin-ajax bypass.
Task 10: For Apache + ModSecurity, confirm which ruleset is loaded (CRS) and version hints
cr0x@server:~$ sudo apachectl -M 2>/dev/null | grep -i security
security2_module (shared)
cr0x@server:~$ sudo grep -R --line-number 'owasp-crs' /etc/apache2 | head
/etc/apache2/mods-enabled/security2.conf:15:IncludeOptional /usr/share/modsecurity-crs/owasp-crs.load
What it means: Youâre using OWASP CRS. Rule IDs like 942100 map to CRS SQLi.
Decision: Use CRS-supported exclusion mechanisms; avoid editing vendor rule files directly (it makes upgrades miserable).
Task 11: Create a narrow ModSecurity exclusion (example) and validate syntax
cr0x@server:~$ sudo tee /etc/nginx/modsec/wordpress-exclusions.conf >/dev/null <<'EOF'
# Narrow exclusion: only for admin-ajax.php, only remove one parameter from one rule
SecRule REQUEST_URI "@streq /wp-admin/admin-ajax.php" "id:1001001,phase:1,pass,nolog,ctl:ruleRemoveTargetById=942100;ARGS:query"
EOF
cr0x@server:~$ sudo grep -R --line-number 'wordpress-exclusions' /etc/nginx/modsec/main.conf
72:Include /etc/nginx/modsec/wordpress-exclusions.conf
cr0x@server:~$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
What it means: You removed the query parameter from inspection by rule 942100 only on that URI. Everything else still gets inspected.
Decision: Reload and test. If attacks shift to other parameters, youâll see new rule hits. Thatâs fine; youâre iterating with evidence.
Task 12: Reload and retest the exact failing request
cr0x@server:~$ sudo systemctl reload nginx
cr0x@server:~$ curl -kisS -X POST https://example.com/wp-admin/admin-ajax.php \
-d 'action=test_action&_ajax_nonce=deadbeef&query=union%20select' | sed -n '1,25p'
HTTP/2 200
date: Fri, 27 Dec 2025 10:19:44 GMT
content-type: text/html; charset=UTF-8
What it means: The WAF no longer blocks this request. Now you must ensure WordPress rejects bad nonces and unauthenticated behavior as intended.
Decision: Confirm the application layer still validates auth/nonce; if it doesnât, your WAF âfixâ just became an exploit lane.
Task 13: Verify WordPress still blocks unauthenticated dangerous actions
cr0x@server:~$ curl -kisS -X POST https://example.com/wp-admin/admin-ajax.php \
-d 'action=delete_user&user_id=1' | sed -n '1,30p'
HTTP/2 200
date: Fri, 27 Dec 2025 10:20:12 GMT
content-type: text/html; charset=UTF-8
0
What it means: Many WordPress AJAX endpoints return 0 for unauthorized. Thatâs good-ish; it means youâre reaching WordPress and itâs denying the action.
Decision: If you see a successful response for privileged actions without auth, stop. Roll back the exclusion and fix auth/nonce enforcement first.
Task 14: Check whether big cookies are tripping header limits (a sneaky WAF trigger)
cr0x@server:~$ curl -kisS https://example.com/ | awk 'tolower($0) ~ /^set-cookie:/ {print length($0), $0}' | head
148 Set-Cookie: wordpress_test_cookie=WP%20Cookie%20check; path=/
312 Set-Cookie: wp-settings-1=libraryContent%3Dbrowse; path=/; expires=Sat, 26 Dec 2026 10:20:55 GMT
What it means: Not huge, but if you see very long cookie lines (thousands of bytes), some WAFs/proxies will reject requests later when Cookie headers balloon.
Decision: If cookies are massive, reduce cookie bloat (plugin cleanup, scope cookies, avoid stuffing state into cookies) rather than tuning WAF to accept absurd headers.
Task 15: Detect 429s (rate limit) vs 403s (signature) in logs
cr0x@server:~$ sudo awk '{print $9}' /var/log/nginx/access.log | sort | uniq -c | sort -nr | head
981 200
114 403
62 404
27 429
What it means: Some requests are rate limited (429). Thatâs a different tuning path than false-positive signature blocking.
Decision: If editors are getting 429 during normal admin work, adjust rate limits by endpoint and method. Donât weaken SQLi/XSS rules to solve rate limiting.
Task 16: Confirm real client IP preservation (WAF tuning fails if IPs are wrong)
cr0x@server:~$ sudo grep -R --line-number 'real_ip_header|set_real_ip_from' /etc/nginx | head -n 20
/etc/nginx/conf.d/realip.conf:1:real_ip_header CF-Connecting-IP;
/etc/nginx/conf.d/realip.conf:2:set_real_ip_from 173.245.48.0/20;
/etc/nginx/conf.d/realip.conf:3:set_real_ip_from 103.21.244.0/22;
What it means: Nginx is trusting a header and specific proxy IP ranges. Without this, rate limits and WAF correlation can be nonsense.
Decision: If you see all clients as the proxy IP, fix real IP handling before tuning behavioral rules.
Tuning patterns that work (and what they cost)
Pattern A: Exclude one parameter from one rule on one endpoint
This is the gold standard for false positives. You keep the rule active everywhere else. You keep other rules active on that endpoint. You also retain audit logging.
Best for: Gutenberg/editor content fields, search fields, JSON payload keys that happen to look like code.
Cost: You have to maintain a small map of âknown noisy parameters.â Thatâs not a real cost; itâs called owning your system.
Pattern B: Separate policies for admin vs public traffic
Admin endpoints behave differently. They should also be protected differently. In many orgs the best move is to put admin behind additional controls:
- Require MFA / SSO
- Restrict by VPN or corporate egress IPs (if feasible)
- Tighter rate limits on login; looser on editor POST bodies where legitimate HTML exists
Best for: sites with many editors and heavy admin usage.
Cost: More policy objects and testing. Still cheaper than downtime and panic-whitelisting.
Pattern C: Move managed rules from âblockâ to âcountâ temporarilyâsurgically
Sometimes you donât know if itâs a false positive until you collect more events. Put one managed rule group into count/detect-only for one endpoint for a short window.
Best for: incidents where business impact is high and the rule match evidence is unclear.
Cost: Short-term risk. You must compensate with rate limiting and monitoring during the window.
Pattern D: Block what you donât use (this reduces both risk and WAF noise)
If you donât need /xmlrpc.php, block it at the edge or origin. Same for unused REST routes or legacy endpoints. Less surface area means fewer weird payloads and fewer rule collisions.
Joke #2: XML-RPC is like that old printer in the cornerânobody admits they use it, but it still causes incidents.
Three corporate mini-stories from real life systems
Mini-story 1: The incident caused by a wrong assumption
A mid-sized company moved their marketing site to WordPress behind a managed WAF. The migration checklist said âturn on managed rules, set to block, ship it.â The assumption was that managed rules are âsafe by default.â They usually are. Until they arenât.
The first Monday, editors started using a new page builder plugin that saved layouts as large JSON blobs via /wp-json/. The WAF began blocking requests as âprotocol attack: invalid JSONâ and âXSS in bodyâ because the builder stored HTML fragments and inline styles inside JSON strings.
Engineering assumed it was a WordPress bug. Marketing assumed engineering was âblocking creativity.â Security assumed marketing was uploading malware. Everyone was wrong in their own special way.
The fix was not disabling the WAF. They pulled the WAF event logs, found two noisy rules, and excluded specific JSON keys on the /wp-json/ route while keeping XSS rules active elsewhere. They also added a size cap and normalized encoding at the application edge. The incident ended, and the argument ended later.
Mini-story 2: The optimization that backfired
A larger org decided to âoptimize performanceâ by caching more aggressively at the CDN. They added a rule: cache everything under /wp-json/ for 10 minutes. They also kept the WAF strict on APIs because âAPIs are dangerous.â Two decisions, both reasonable, combined into chaos.
Editors saw random authorization failures and intermittent 403s. Some requests were cached 200s (from an authenticated context) and served to unauthenticated clients, which triggered WAF bot checks and policy mismatches. Meanwhile, WordPress nonces were expiring in unpredictable ways because the UI was now mixing cached and live responses.
Engineering first tuned the WAF, because 403 meant âWAF problem.â That tuning made it worse: now the wrong cached content flowed more freely. The real root cause was caching an endpoint that should never have been cached without careful vary rules and authentication segmentation.
They rolled back caching for authenticated REST routes, kept caching only for explicitly public GET routes, and tightened WAF rules around login and XML-RPC. Performance improved, error rates dropped, and everyone learned the same lesson we keep relearning: âfasterâ is not a feature if itâs wrong.
Mini-story 3: The boring but correct practice that saved the day
A regulated business ran WordPress for a customer portal. Their security team required every WAF change to go through a small pipeline: log-only for 24 hours, review top matches, then enforce. It sounded bureaucratic. It was also the reason they slept at night.
One day, a plugin update changed an admin form to submit content with a parameter named template containing chunks of HTML and shortcodes. The WAF started matching it as âPHP injectionâ on admin pages. Because the WAF change process included baseline dashboards and a staging mode, they noticed the spike before turning the updated rule to block in production.
They implemented an exclusion scoped to authenticated admin URIs and the specific parameter, then added a unit test that posts the exact payload through the WAF in CI. Boring. Predictable. Effective.
When an auditor later asked âhow do you prevent emergency disablement of security controls,â they had an answer that wasnât a promise. It was evidence.
Common mistakes: symptom â root cause â fix
1) Editors canât save posts; Gutenberg shows âUpdating failedâ
Symptom: intermittent 403/406 on /wp-json/ POST requests.
Root cause: WAF rule matches on JSON body (HTML fragments, inline scripts, or CSS) or strict JSON parser rules.
Fix: Exclude specific JSON keys/parameters for specific rule IDs on /wp-json/ POST, keep logging. Confirm WordPress auth and nonce validation still applies.
2) Media uploads fail with 403; only large files fail
Symptom: small files upload, big ones fail; sometimes 413 appears too.
Root cause: request body size limits at WAF/CDN/origin; multipart parsing rules; antivirus integration timeouts.
Fix: Align limits across layers (CDN, WAF, Nginx/Apache, PHP). Prefer increasing limits rather than bypassing inspection for uploads; if you must bypass, add strict file type checks and malware scanning.
3) wp-admin works on VPN but not from home networks
Symptom: 403 only for some ISPs/regions; admin appears as âbot.â
Root cause: bot protection or reputation rules too aggressive; missing challenges/cookies due to privacy settings; or your origin misreads client IP and triggers rate limits.
Fix: Fix real IP handling; tune bot rules specifically for /wp-login.php and /wp-admin/; consider step-up challenges only on login.
4) REST API GET requests are blocked but normal pages work
Symptom: public site loads, but integrations fail.
Root cause: managed âAPI protectionâ rule group blocks patterns common in REST query parameters.
Fix: Allowlist specific REST routes and methods; exclude only the offending parameter names from specific rule IDs; add rate limiting and auth checks for sensitive endpoints.
5) Everything under /wp-admin/ is whitelisted âtemporarily,â then never reverted
Symptom: security team complains later; you find a months-old bypass rule.
Root cause: incident fix without a rollback plan; no expiry on emergency policies.
Fix: Add explicit expiration for emergency exceptions, plus alerting for âbypass rules present.â Bake it into change management, not human memory.
6) WAF blocks legitimate HTML in post content
Symptom: saving a post triggers XSS rules.
Root cause: WAF sees HTML and thinks âattack.â WordPress content is literally HTML. Shocking, I know.
Fix: Exclude content fields for editor endpoints only; ensure WordPressâs KSES/HTML sanitization is enabled and youâre not granting unfiltered_html to everyone.
7) Login brute force still succeeds after tuning
Symptom: tuning reduced blocks but increased attack success.
Root cause: you weakened auth-related protections (rate limiting, bot checks) while chasing false positives elsewhere.
Fix: Separate policies: strict on /wp-login.php, /xmlrpc.php, and admin; measured exceptions on editor/REST payloads.
Checklists / step-by-step plan
Step-by-step plan for tuning without creating a hole
- Capture one failing request: timestamp, client IP, URI, method, request ID (edge trace header if present).
- Prove the blocking layer: edge vs origin using
curl --resolveor direct origin access from a trusted network segment. - Pull the WAF event: rule ID/group, matched variable, payload snippet, action taken (block/challenge/log).
- Classify the event:
- Looks like attack from random IPs? Treat as true positive.
- Happens to authenticated editors across many IPs? Likely false positive.
- Mostly 429? Itâs rate limiting, not signatures.
- Pick the smallest lever:
- Exclude one parameter target from one rule ID on one URI, or
- Scope exception to method + URI + authenticated context, or
- Temporarily count-only for one group on one endpoint while gathering evidence.
- Add compensating controls when you loosen inspection:
- Ensure WordPress nonce/auth checks are enforced
- Rate limit login and admin-ajax by IP/user
- Block unused endpoints like XML-RPC if not required
- Test the exact failure and a few abuse cases: repeat the failing request; also try obvious payloads and ensure they still block somewhere appropriate.
- Monitor for drift: new top WAF matches, increased 5xx, increased auth failures, or sudden drops in blocks (a sign you over-bypassed).
- Document the exception: endpoint, rule ID, reason, owner, expiry date, and rollback plan.
Operational checklist for production readiness
- WAF logs are accessible, searchable, and retained long enough to debug incidents.
- Admin endpoints have stronger controls than public endpoints (policy separation).
- Emergency bypass rules have expiration and alerting.
- Rate limiting exists for
/wp-login.phpand/xmlrpc.php. - Real client IP is correctly propagated to origin logs and security tools.
- Staging can reproduce WAF behavior (or you have a safe âcount-onlyâ window for evidence collection).
FAQ
1) Should I just whitelist /wp-admin/ in the WAF?
No. Thatâs where the most valuable actions live. If you must reduce friction, scope exceptions to specific admin endpoints and parameters, and keep login protections strict.
2) Is it safe to disable one OWASP CRS rule ID?
Sometimes, but do it like surgery: disable or target-remove the smallest piece, and only for the endpoint where itâs proven noisy. Donât âruleRemoveByIdâ globally unless you enjoy explainers after incidents.
3) Why does admin-ajax.php trigger so many WAF rules?
Because itâs a high-traffic POST endpoint with lots of parameters and plugin-defined actions. Attackers love it, plugins abuse it, and WAFs notice both.
4) Should I block xmlrpc.php?
If you donât explicitly need it, yesâblock it at the edge and origin. If you do need it, heavily rate limit it and consider allowing only known clients.
5) How do I know if itâs a false positive or a real attack?
Look at context: is it authenticated editor traffic, consistent user agents, and reproducible from normal workflows? Or is it random IP spray with obvious payloads? Use audit logs: matched variable + payload snippet usually makes it clear.
6) Can caching cause WAF blocks?
Indirectly, yes. Bad caching of authenticated REST responses can create weird mismatches and repeated retries that trip bot/rate rules. Fix caching strategy before you loosen security rules.
7) How do I tune WAF rules without losing visibility?
Keep logging enabled even when you exclude targets. Use count/detect-only modes for short evidence windows. Build dashboards for top rule IDs and affected URIs so exceptions donât become invisible forever.
8) Whatâs the safest way to handle editor HTML that triggers XSS rules?
Exclude the specific content field on the specific editor endpoint from specific XSS rules, and ensure WordPress sanitization and user capabilities are correctly configured.
9) What if the WAF vendor wonât show me the matched payload?
Then you need better logging at some layer you control (origin WAF audit logs, reverse proxy debug logs with care, or a staging environment). Tuning without match data is guessing with paperwork.
Practical next steps
Do three things today, in this order:
- Get traceability: ensure every block gives you a request ID and a rule ID you can search. If thatâs not true, fix logging before the next incident forces you to âtemporary bypassâ yourself into a corner.
- Split policy by endpoint: treat
/wp-login.php,/xmlrpc.php,/wp-admin/,/wp-json/, and/admin-ajax.phpas different products with different risk profiles. - Implement narrow exclusions with expiry: exclude parameters from specific rules, not whole rule groups. Add an owner and an expiration date, and make rollback boring.
Your WAF should be a scalpel, not a blindfold. If itâs blocking WordPress, you donât need less securityâyou need better fit.