Introduction
Apache's mod_security web application firewall (WAF) uses rules from the OWASP Core Rule Set (CRS) to detect and block attacks. However, legitimate POST requests containing code snippets, JSON payloads, or unusual patterns can trigger false positives, resulting in 403 Forbidden responses. The error log shows:
[Tue Apr 09 01:00:00.123456 2026] [security2:error] [pid 1234] [client 192.168.1.50] ModSecurity: Access denied with code 403 (phase 2). Matched "Operator `Ge' with parameter `5' against `TX:ANOMALY_SCORE'" [file "/etc/modsecurity/crs/rules/REQUEST-941-APPLICATION-ATTACK-XSS.conf"] [line "123"] [id "941100"] [msg "XSS Filter - Category 1: Script Tag Vector"]Symptoms
- Legitimate POST requests receive 403 Forbidden
- Error log shows ModSecurity rule ID and matching pattern
- Form submissions with HTML content, code, or special characters are blocked
- API requests with JSON containing SQL-like patterns are rejected
- The blocked request works when mod_security is temporarily disabled
Common Causes
- OWASP CRS rules are too strict for the application's legitimate traffic patterns
- Rule triggers on HTML in WYSIWYG editor content or markdown submissions
- JSON API payloads contain patterns matching SQL injection or XSS rules
- Rule engine set to
On(blocking) instead ofDetectionOnlyduring tuning - Application-specific patterns (e.g., code sharing platform) inherently trigger security rules
Step-by-Step Fix
- 1.Identify the triggering rule from the ModSecurity audit log at
/var/log/modsec_audit.log: - 2.```bash
- 3.tail -100 /var/log/modsec_audit.log | grep -B5 "Access denied"
- 4.
` - 5.Note the rule ID (e.g.,
941100) and the request that triggered it. - 6.Disable the specific rule for the affected path:
- 7.```apache
- 8.<Location "/api/posts">
- 9.SecRuleRemoveById 941100
- 10.SecRuleRemoveById 941110
- 11.</Location>
- 12.
` - 13.This disables only the specific XSS rules for the API endpoint that legitimately receives HTML content.
- 14.Adjust the paranoia level if too many rules are triggering. Edit
/etc/modsecurity/crs/crs-setup.conf: - 15.```apache
- 16.# Lower from default 1 to be less aggressive
- 17.setvar:tx.paranoia_level=1
- 18.
` - 19.Paranoia levels: 1 (default, fewer rules), 2 (more rules), 3 (aggressive), 4 (very aggressive).
- 20.Switch to DetectionOnly mode while tuning:
- 21.```apache
- 22.SecRuleEngine DetectionOnly
- 23.
` - 24.This logs violations without blocking them, allowing you to analyze false positives without affecting users.
- 25.Create a whitelist rule for trusted request patterns:
- 26.```apache
- 27.# Allow JSON content type for API endpoints
- 28.SecRule REQUEST_URI "^/api/" "id:10001,phase:1,pass,nolog,ctl:ruleRemoveTargetById=941100;REQUEST_BODY"
- 29.
` - 30.This exempts API endpoint request bodies from XSS rule inspection.
- 31.Test the fix by replaying the previously blocked request:
- 32.```bash
- 33.curl -X POST http://example.com/api/posts \
- 34.-H "Content-Type: application/json" \
- 35.-d '{"title": "<script>alert(1)</script>", "body": "test post"}' \
- 36.-v 2>&1 | grep "< HTTP"
- 37.
` - 38.After the rule exception, the request should return
HTTP/1.1 200 OKor the application's expected response.
Prevention
- Run mod_security in
DetectionOnlymode for at least one week after deployment to collect false positive data - Analyze audit logs weekly and create targeted rule exceptions for legitimate application patterns
- Document all rule exceptions with the reason and date added in your configuration comments
- Use
SecAuditEngine RelevantOnlyto reduce audit log volume while still capturing blocked requests - Consider implementing mod_security rules per-VirtualHost rather than globally for fine-grained control
- Keep OWASP CRS updated but test rule updates in staging before production deployment