WordPress blocked by WAF: tune rules without opening security holes

Was this helpful?

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.php for auth
  • /wp-admin/admin-ajax.php for asynchronous UI behavior (frontend and backend)
  • /wp-json/ for the REST API (including Gutenberg/editor features)
  • /xmlrpc.php historically 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

  1. Fix the app/request if it’s legitimately weird (bad plugin, broken encoding, huge cookies).
  2. Scope down using URI + method + authentication state (admin only) before you consider disabling a rule.
  3. Exclude the smallest variable (a single parameter) rather than an entire rule group.
  4. Raise anomaly thresholds only when you can prove the traffic is safe and you have compensating controls.

Interesting facts and context (because history repeats)

  1. ModSecurity started life in the early 2000s as an Apache module—WAFs were originally a “hot patch” for vulnerable apps, not a silver bullet.
  2. The OWASP Core Rule Set (CRS) became the default “generic” WAF brain for many stacks; it’s designed to be broad, not WordPress-specific.
  3. 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.
  4. Gutenberg (the block editor) shifted WordPress toward heavier REST API usage; many “sudden” WAF issues appeared after editor upgrades because request shapes changed.
  5. Early WAF deployments often ran in “detect only” mode for weeks; blocking immediately caused outages. Mature teams still stage WAF changes like code.
  6. “Positive security” models (allow known-good patterns) predate modern WAF marketing; they’re harder to run, but more robust for admin endpoints.
  7. Some SQLi detection signatures are decades old; attackers adapt, but so do applications—false positives often come from modern JSON bodies tripping legacy patterns.
  8. 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 GET but scrutinize POST)
  • Authentication context (admin cookie present, or requests from VPN, or from a known IdP-protected admin)
  • Parameter name (exclude content field, 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

  1. Capture one failing request: timestamp, client IP, URI, method, request ID (edge trace header if present).
  2. Prove the blocking layer: edge vs origin using curl --resolve or direct origin access from a trusted network segment.
  3. Pull the WAF event: rule ID/group, matched variable, payload snippet, action taken (block/challenge/log).
  4. 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.
  5. 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.
  6. 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
  7. Test the exact failure and a few abuse cases: repeat the failing request; also try obvious payloads and ensure they still block somewhere appropriate.
  8. Monitor for drift: new top WAF matches, increased 5xx, increased auth failures, or sudden drops in blocks (a sign you over-bypassed).
  9. 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.php and /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:

  1. 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.
  2. Split policy by endpoint: treat /wp-login.php, /xmlrpc.php, /wp-admin/, /wp-json/, and /admin-ajax.php as different products with different risk profiles.
  3. 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.

← Previous
Card grids that auto-fit columns with auto-fit/minmax (no media queries)
Next →
ZFS vs mdadm: Where mdraid Wins and Where It Loses

Leave a comment