ModSecurity blocking a legitimate application (HTTP 403)#
Environment: cPanel/WHM server on AlmaLinux/CloudLinux Symptom: Legitimate requests returning HTTP 403 Forbidden for a specific domain Objective: Diagnose the block and disable ModSecurity for the domain or configure a fine-grained rule exception via SSH (without the WHM graphical interface)
The 403 diagnosis in Apache logs at /usr/local/apache/logs/error_log:
[Wed May 01 21:40:00.123456 2025] [:error] [pid 12345] [client 203.0.113.45:52341]
ModSecurity: Access denied with code 403 (phase 2).
Pattern match "(?i)(?:\\b(?:(?:s(?:elect\\b(?:.{1,100}?\\b(?:(?:length|count|top)\\b.
{1,100}?\\bfrom|from\\b.{1,100}?\\bwhere)|.*?\\b(?:d(?:ump\\b.*\\bfrom|ata_type)|
(?:to_(?:numbe|cha)|inst)r))|p_(?:(?:addextendedpro|sqlexe)c|(?:oacreat|prepar)e|
execute(?:sql)?|makewebtask)|ql_(?:longvarchar|variant))..." at ARGS:description.
[file "/etc/apache2/conf.d/modsec_vendor_configs/OWASP3/rules/REQUEST-942-APPLICATION-ATTACK-SQLI.rlz"]
[line "45"] [id "942100"] [rev ""] [msg "SQL Injection Attack Detected via libinjection"]
[data "Matched Data: ..."] [severity "CRITICAL"] [ver "OWASP_CRS/3.3.4"]
[hostname "mydomain.com"] [uri "/admin/api/content"] [unique_id "YzX9..."]
The application uses free-text fields that trigger the OWASP rule 942100 (SQL Injection via libinjection). The false positive is a legitimate description field with SQL syntax in technical documentation.
The error: anatomy of the whmapi1 failure#
The first attempt was to use the WHM administrative API, the natural intuition for anyone managing via CLI:
whmapi1 toggle_modsec_for_domain domain=mydomain.com state=0
Complete API return:
---
metadata:
command: toggle_modsec_for_domain
reason: Unknown app ("toggle_modsec_for_domain") requested for this version (1) of the API.
result: 0
version: 1
The reason field is explicit: Unknown app ("toggle_modsec_for_domain"). The endpoint does not exist in whmapi1.
Architectural analysis: WHM API vs. UAPI - scope separation#
The confusion is understandable because both WHM and cPanel manage ModSecurity, but at different layers of the hierarchy:
WHM (Web Host Manager) - Scope: SERVER
│
│ whmapi1 / whm_api2
│ ├── Create/remove cPanel accounts
│ ├── Restart services (Apache, MySQL, etc.)
│ ├── Configure IP addresses
│ ├── Manage ModSecurity GLOBALLY (enable/disable server-wide)
│ └── ❌ Does NOT manage per-domain/vhost configurations
│
└── cPanel - Scope: ACCOUNT (user/domain)
│
│ UAPI (uapi) - Current v3 API
│ ├── Manage addon and parked domains
│ ├── Create email accounts
│ ├── Manage databases
│ └── ✅ ModSecurity module: enable/disable per domain
Why this separation exists:
Per-domain ModSecurity is implemented through directives in the Apache VirtualHost specific to each cPanel account. The line in the httpd.conf generated by cPanel/EasyApache is based on include files. whmapi1 does not have access to operate in individual account context - that is the exclusive domain of the UAPI.
Security protocol and practical solution#
Step 1 - identify the domain owner#
/scripts/whoowns mydomain.com
Output: myuser
The /scripts/whoowns script queries the /etc/userdomains file - a flat domain→user map maintained by cPanel:
# Alternative manual verification
grep "mydomain.com" /etc/userdomains
# mydomain.com: myuser
Step 2 - false positive validation and initial triage#
Before making modifications, validate whether the endpoint actually triggers the block with legitimate traffic using curl:
curl -X POST https://mydomain.com/admin/api/content \
-H "Content-Type: application/json" \
-d '{"description": "test"}' \
-v 2>&1 | grep -i "403\|modsecurity"
Verify if rule 942100 is the single or primary cause of blocks on the target domain:
grep "mydomain.com" /usr/local/apache/logs/error_log | grep "942100" | wc -l
Step 3 - pre-configuration backup protocol#
Never alter configuration files or run rebuild commands without ensuring restore points:
# Create a complete backup of the userdata directory
tar czf /root/modsec-backup-$(date +%Y%m%d).tar.gz /etc/apache2/conf.d/userdata/
# Back up the domain-specific modsec file if it already exists
cp /etc/apache2/conf.d/userdata/ssl/2/myuser/mydomain.com/modsec.conf \
/root/modsec.conf.bak.$(date +%Y%m%d) 2>/dev/null || echo "No existing modsec.conf file found for this domain."
Step 4 - recommended alternative: fine-grained rule exception#
Instead of completely disabling ModSecurity (which would expose the application to multiple attacks), edit or create the userdata file to apply an exception only for rule 942100.
Directory for HTTPS: /etc/apache2/conf.d/userdata/ssl/2/myuser/mydomain.com/modsec.conf
Directory for HTTP: /etc/apache2/conf.d/userdata/2/myuser/mydomain.com/modsec.conf
Example of Exception for the Entire VHost:
<IfModule mod_security2.c>
# Disable only rule 942100 (SQL Injection via libinjection)
SecRuleRemoveById 942100
</IfModule>
Example of Exception Restricted to the Problematic Endpoint (Even More Secure):
<IfModule mod_security2.c>
# Disable the rule only within the corresponding URI
<LocationMatch "/admin/api/content">
SecRuleRemoveById 942100
</LocationMatch>
</IfModule>
After editing the files above, rebuild Apache's configurations and reload the daemon:
# Validate and rebuild Apache configuration
/scripts/rebuildhttpdconf
# Apply changes without dropping active connections
/usr/sbin/apachectl graceful
Alternative solution: UAPI for complete disabling (last resort)#
If you must completely disable ModSecurity for this domain, execute the UAPI in the context of the identified user:
uapi --user=myuser ModSecurity disable_domains domains=mydomain.com
Complete success return:
---
apiversion: 3
func: disable_domains
module: ModSecurity
result:
data:
-
dependencies: []
domain: mydomain.com
enabled: 0
exception: ~
searchhint: ''
type: main
errors: ~
messages: ~
metadata:
transformed: 1
status: 1
warnings: ~
What happens internally after the UAPI command#
cPanel performs the following automated sequence of operations:
1. Validates that myuser exists and owns mydomain.com
2. Creates (or overwrites) the file:
/etc/apache2/conf.d/userdata/ssl/2/myuser/mydomain.com/modsec.conf
3. Writes the SecRuleEngine Off directive:
<IfModule mod_security2.c>
SecRuleEngine Off
</IfModule>
4. Replicates the directive for the HTTP version (non-SSL):
/etc/apache2/conf.d/userdata/2/myuser/mydomain.com/modsec.conf
5. Automatically executes: /scripts/rebuildhttpdconf
6. Executes: /usr/sbin/apachectl graceful ← reload without connection drops
Post-configuration verification and monitoring#
After making the change (either via fine-grained exception or disabling), follow these technical validation steps:
- Test the endpoint that was blocked:
curl -I https://mydomain.com/admin/api/content
# Must return HTTP 200 OK (or the expected response code, no longer 403)
- Verify that other security rules remain active (Shielding Test):
If you used the Fine-Grained Exception Approach, send an alternative malicious payload (such as an XSS script tag) to ensure the WAF still blocks invalid attempts:
curl -X POST https://mydomain.com/admin/api/content \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "payload=<script>alert(1)</script>" \
-w "%{http_code}\n" -o /dev/null
# Must return 403 (ModSecurity blocking XSS correctly)
- Check logs for new errors:
tail -50 /usr/local/apache/logs/error_log | grep "mydomain.com"
Advanced systemic triage and complementary modules#
1. Auditing other domains with disabled ModSecurity#
Check for other WAF security holes on the same user account:
uapi --user=myuser ModSecurity list_domains | grep -B1 -A2 "enabled: 0"
Check statistics for the most frequently triggered rules on the server to understand the global impact of CRS rules:
grep "id \"" /usr/local/apache/logs/error_log | awk -F'id "' '{print $2}' | awk -F'"' '{print $1}' | sort | uniq -c | sort -rn | head -15
2. Checking additional Apache modules#
Sometimes, the client continues to receive HTTP 403 errors or timeouts due to other active security modules in Apache that police connections. Audit which modules are enabled:
apachectl -M | grep -E "evasive|limitipconn|reqtimeout"
- mod_evasive: Limits accesses per IP and URI to prevent DoS/DDoS.
- mod_limitipconn: Limits simultaneous connections per IP.
- mod_reqtimeout: Manages header and request body timeouts.
3. ModSecurity audit logging configuration#
If Apache error logs do not show enough detail to diagnose complex false positives, ensure detailed ModSecurity auditing is active for the request:
# In /etc/apache2/conf.d/modsec2.conf or WHM ModSecurity Configure page
SecAuditEngine RelevantOnly
SecAuditLogRelevantStatus "^(?:5|4(?!04))"
SecAuditLogParts ABIJDEFHX
SecAuditLog /var/log/apache2/modsec_audit.log
SecAuditLogType Serial
4. Version check#
Always document the version of the environment and installed CRS rules for syntax compatibility:
# cPanel/WHM version
cat /usr/local/cpanel/version
# Active ModSecurity version in daemon
httpd -M | grep -i security
# Or consult the Apache startup log
grep -i "ModSecurity" /usr/local/apache/logs/error_log | head -n 5
Visual decision flow#
ModSecurity troubleshooting checklist#
## Checklist: ModSecurity 403 - diagnosis and fix
### 1. Diagnosis and triage
- [ ] Check Apache error logs: `tail -100 /usr/local/apache/logs/error_log | grep 403`
- [ ] Identify the blocking CRS rule (ID and corresponding file)
- [ ] Validate the false positive by reproducing the request via `curl`
- [ ] Check if other domains on the account show the same behavior
### 2. Solution and rule modeling
- [ ] Create an exception for the specific rule via `SecRuleRemoveById` (preferred)
- [ ] Restrict the exception to the correct endpoint using `<LocationMatch>`
- [ ] Completely disable ModSecurity for the domain (only as a last resort)
### 3. Secure implementation
- [ ] Generate a complete backup of userdata directories: `tar czf /root/modsec-backup.tar.gz /etc/apache2/conf.d/userdata/`
- [ ] Create or modify the `modsec.conf` file in the appropriate userdata path (SSL and non-SSL)
- [ ] Rebuild vhosts configuration: `/scripts/rebuildhttpdconf`
- [ ] Gracefully reload Apache: `/usr/sbin/apachectl graceful`
### 4. Technical validation
- [ ] Test the problematic URI and verify the expected HTTP response code
- [ ] Audit error logs to ensure no new blocks occur on the request
- [ ] Ensure blocking remains active against other vectors (such as XSS with `<script>`)
- [ ] Monitor domain stability for 24 hours
### 5. Documentation and post-configuration audit
- [ ] Record the exception created, reason for the false positive, and review date
- [ ] Confirm the domain status via UAPI
- [ ] Test and document the rollback procedure
Rollback plan#
In case of instability, syntax errors detected during Apache restart, or new security requirements, follow this rollback guide:
Manual rollback (restore from userdata backup)#
Restore files from the backup copy created before making any changes:
# Restore the individual file
cp /root/modsec.conf.bak.YYYYMMDD /etc/apache2/conf.d/userdata/ssl/2/myuser/mydomain.com/modsec.conf
# If the file did not originally exist, delete the created file to remove the exception:
rm -f /etc/apache2/conf.d/userdata/ssl/2/myuser/mydomain.com/modsec.conf
rm -f /etc/apache2/conf.d/userdata/2/myuser/mydomain.com/modsec.conf
# Rebuild and reload Apache
/scripts/rebuildhttpdconf
/usr/sbin/apachectl graceful
Rollback via cPanel API (re-enable ModSecurity on domain)#
If you used the UAPI to disable ModSecurity, you can re-enable it with:
uapi --user=myuser ModSecurity enable_domains domains=mydomain.com
Reference table: whmapi1 vs. UAPI for ModSecurity#
| Action | Correct tool | Command |
|---|---|---|
| Disable for 1 domain | uapi | uapi --user=X ModSecurity disable_domains domains=Y |
| Re-enable for 1 domain | uapi | uapi --user=X ModSecurity enable_domains domains=Y |
| List status per account | uapi | uapi --user=X ModSecurity list_domains |
| Disable globally | whmapi1 | whmapi1 modsec_disable_all_domains |
| View global configuration | whmapi1 | whmapi1 modsec_get_settings |
| View ModSec audit log | whmapi1 | whmapi1 modsec_get_log |
Was this article helpful?
Leave a quick reaction to help prioritize future technical guides:
This post is licensed under CC BY-NC.



Comments
Join the discussion below.
0 comments