Buzeli
buzeliSoluções Digitais
Security

Why hyphenated WordPress slugs trip the OWASP CRS on your WAF (and how to exclude the parameter)

Published on July 27, 2026

Uma URL com hifens disparando por engano uma regra de SQL injection num WAF, e uma exceção liberando o parâmetro

The symptom: legitimate blocks on the WAF

Auditing 24h of WAF logs, 13 requests were blocked. Two were a real attack (a scanner hunting for .env). The other 11 were legitimate traffic from the site itself: Next.js ISR/revalidation calls to the WordPress API with query string ?slug=some-long-title-with-many-hyphens.

Copy
# block distribution by rule (24h)
ruleId 942432  → 7   (SQLi - special chars)
ruleId 942431  → 2   (SQLi - char anomaly)
ruleId 930130  → 2   (LFI)
.env scanner   → 2   (real attack — keep blocked)

The cause: hyphens score as SQLi anomaly

OWASP CRS's SQL-injection rules score special characters and the 'anomaly' of a string. A long WordPress slug — many hyphens, numbers, concatenated words — accumulates enough points to cross the 942431/942432 thresholds, even with nothing malicious. The slug parameter simply wasn't in the CRS exclusion list.

OWASP CRS is scoring, not certainty. A benign but 'weird' value (hyphenated slug, base64, JWT) accumulates score and becomes a false positive. The fix is to exclude the specific parameter, not lower global sensitivity.

The fix: exclude slug across ALL 141 capabilities

On OCI, the exclusion must be applied to each of the CRS's 141 capabilities — there's no single toggle. You do it via API/CLI, reading the policy, adding the argumentName to each capability's exclusion, and rewriting with the etag. Since the WAF is large, an inline Python patch is more robust than editing by hand:

Copy
import json, subprocess
pid = "ocid1.waaspolicy.oc1..aaaaEXAMPLE"
pol = json.loads(subprocess.check_output(
    ["oci","waf","web-app-firewall-policy","get","--waf-policy-id",pid]))
data, etag = pol["data"], pol["etag"]

# add 'slug' to the argument-name exclusions of each protection capability
for cap in data["request-protection"]["rules"][0]["protection-capabilities"]:
    ex = cap.setdefault("exclusions", {}).setdefault("args", [])
    if "slug" not in ex:
        ex.append("slug")

subprocess.run(["oci","waf","web-app-firewall-policy","update","--waf-policy-id",pid,
    "--request-protection", json.dumps(data["request-protection"]),
    "--if-match", etag])

Gotcha: if the policy is being modified, the update returns 409 ("being modified") — you must wait and resend with the refreshed etag. We validated with a loop of 5 real long slugs: all returned 200, and the .env scanner stayed blocked.

Lessons

1. Long hyphenated slugs become SQLi false positives. The CRS scores special characters; WordPress permalinks accumulate score without being malicious.

2. Exclude the parameter, don't lower sensitivity. Adding 'slug' to the exclusions preserves protection for the rest of the request.

3. On OCI, exclusion is per-capability (141). There's no single toggle; an API patch with etag (and a 409 retry) applies it to all.

Conclusion

A WAF that blocks your own site is as much a problem as one that lets the attack through. OWASP CRS was right to distrust weird strings — it just didn't know that ?slug= was a post title, not a payload. Excluding the parameter across the 141 capabilities gave ISR back without giving up on blocking the .env scanner that showed up in the same log.