You’ve done everything right. ModSecurity is running, OWASP CRS is loaded, nginx is humming along with HTTP/3 support enabled, and life is good. Then you check your audit logs and see this beauty:
"http_version": 3.0,
"messages": [{
"message": "Request Missing a Host Header",
"ruleId": "920280"
}]
Wait, what? Every. Single. HTTP/3. Request. Is triggering Rule 920280. Your legitimate users are being flagged (or blocked) because apparently they all forgot to include a Host header.
Spoiler alert: They didn’t forget anything. ModSecurity just can’t see it.
Welcome to the wonderful world of HTTP/3 pseudo-headers and WAF compatibility issues. Grab a coffee – this one’s going to require some protocol-level thinking.
The Problem
When :authority becomes invisible
Here’s the deal: HTTP/3 (and HTTP/2, actually) don’t use the traditional Host header. Instead, they use a pseudo-header called :authority. It contains the same information, just with a different name and format.
| Protocol | How Host is Sent | Example |
|---|---|---|
| HTTP/1.1 | Host: example.com |
Regular header |
| HTTP/2 | :authority: example.com |
Pseudo-header |
| HTTP/3 | :authority: example.com |
Pseudo-header |
nginx understands this perfectly. It reads :authority and internally treats it as the Host. Your backend servers work fine. Your virtual hosts resolve correctly. Everything is beautiful.
Except ModSecurity doesn’t get the memo.
The ModSecurity-nginx connector extracts headers from nginx’s internal structures, but it doesn’t translate :authority into Host. So when ModSecurity looks for REQUEST_HEADERS:Host, it finds… nothing.
ModSecurity sees HTTP/3 request:
┌─────────────────────────────────────────┐
│ REQUEST_HEADERS:Host = (empty) │ ← Problem!
│ REQUEST_URI = /@fs/.env │ ← Works fine
│ REQUEST_METHOD = GET │ ← Works fine
│ ARGS = (whatever) │ ← Works fine
└─────────────────────────────────────────┘
Fun fact: This issue has been known since at least 2020. A fix was partially implemented in 2022 for protocol version detection, but the Host header translation still isn’t complete as of 2026.
Which Rules Are Affected
The good news: It’s only three rules
Here’s the silver lining: out of hundreds of CRS rules, only three directly check REQUEST_HEADERS:Host:
| Rule ID | What It Checks | Impact on HTTP/3 |
|---|---|---|
| 920280 | Host header exists | 🔴 False positive on ALL requests |
| 920290 | Host header not empty | 🟠 May trigger |
| 920350 | Host header is not an IP | ❌ Can’t function |
Everything else – SQL injection, XSS, LFI, RCE, PHP injection – works perfectly fine because those rules check REQUEST_URI, ARGS, REQUEST_BODY, and other variables that ARE available in HTTP/3.
Rules that DON'T need Host header:
├── 930xxx - LFI/RFI rules ✅
├── 931xxx - RFI rules ✅
├── 932xxx - RCE rules ✅
├── 933xxx - PHP injection ✅
├── 941xxx - XSS rules ✅
├── 942xxx - SQL injection ✅
├── 943xxx - Session fixation ✅
└── 944xxx - Java attacks ✅
Pro tip: If you’re writing custom ModSecurity rules, stick to REQUEST_URI instead of REQUEST_HEADERS:Host. Your rules will work across HTTP/1.1, HTTP/2, AND HTTP/3.
The Fix
Three options, ranked by elegance
Option 1: Disable the Problematic Rules (Quick & Dirty)
Add this to your custom rules file:
1. Disable Host header checks for HTTP/3 compatibility
SecRuleRemoveById 920280
SecRuleRemoveById 920290
SecRuleRemoveById 920350
✅ Pros: Simple, immediate fix ❌ Cons: Loses these checks entirely (even for HTTP/1.1)
Option 2: Conditionally Disable for HTTP/3 (Smarter)
1. Only disable for HTTP/3 requests
SecRule REQUEST_PROTOCOL "@streq HTTP/3.0" \
"id:900010,\
phase:1,\
pass,\
nolog,\
ctl:ruleRemoveById=920280,\
ctl:ruleRemoveById=920290,\
ctl:ruleRemoveById=920350"
✅ Pros: HTTP/1.1 and HTTP/2 still get Host header validation ❌ Cons: Slightly more complex
Option 3: Wait for the Official Fix (Optimistic)
There’s an open PR on the ModSecurity-nginx connector that adds proper :authority → Host translation. When merged, the issue should be resolved.
✅ Pros: Proper fix at the source ❌ Cons: No release date, been open for months
Debugging: How to Confirm the Issue
Trust but verify
Check Your HTTP Version Distribution
tail -1000 /var/log/modsec/audit.log | \
grep -o 'http_version":[0-9.]*' | \
sort | uniq -c
Expected output showing mixed traffic:
348 http_version":1.1
52 http_version":2.0
151 http_version":3.0
Compare HTTP/2 vs HTTP/3 Headers
HTTP/2 request (Host present):
"http_version": 2.0,
"headers": {
"host": "example.com",
"user-agent": "Mozilla/5.0..."
}
HTTP/3 request (Host missing):
"http_version": 3.0,
"headers": {
"user-agent": "Mozilla/5.0...",
"accept": "*/*"
// No host field!
}
Check for Rule 920280 Triggers
grep "920280" /var/log/modsec/audit.log | \
grep -o 'http_version":[0-9.]*' | \
sort | uniq -c
If you see mostly http_version":3.0, you’ve confirmed the issue.
Why This Matters
Beyond false positives
The Host header check (920280) isn’t just bureaucratic overhead. It serves real security purposes:
| Security Purpose | Without Host Header Check |
|---|---|
| Virtual host confusion attacks | ⚠️ Not detected |
| Host header injection | ⚠️ Not detected on HTTP/3 |
| Request smuggling (some variants) | ⚠️ Reduced visibility |
| Compliance requirements | ⚠️ May fail audits |
That said, the real-world impact is relatively low because:
- Modern browsers always send
:authority(just not visible to ModSecurity) - nginx still routes correctly based on
:authority - Most attacks don’t rely on manipulating the Host header
- 99% of security rules still function normally
Quick Reference Card
┌───────────────────────────────────────────────────────────────┐
│ HTTP/3 + MODSECURITY COMPATIBILITY GUIDE │
├───────────────────────────────────────────────────────────────┤
│ │
│ THE PROBLEM: │
│ HTTP/3 uses :authority pseudo-header │
│ ModSecurity-nginx connector doesn't translate it │
│ REQUEST_HEADERS:Host appears empty │
│ │
│ AFFECTED RULES: │
│ 920280 → "Request Missing a Host Header" │
│ 920290 → "Empty Host Header" │
│ 920350 → "Host header is a numeric IP address" │
│ │
│ UNAFFECTED: │
│ All SQLi, XSS, LFI, RCE, PHP rules ✅ │
│ Any rule using REQUEST_URI ✅ │
│ Any rule using ARGS or REQUEST_BODY ✅ │
│ │
│ QUICK FIX: │
│ SecRuleRemoveById 920280 │
│ SecRuleRemoveById 920290 │
│ SecRuleRemoveById 920350 │
│ │
│ SMART FIX: │
│ Use ctl:ruleRemoveById only for HTTP/3.0 requests │
│ │
│ CUSTOM RULES: │
│ Use REQUEST_URI instead of REQUEST_HEADERS:Host │
│ → Works on HTTP/1.1, HTTP/2, AND HTTP/3 │
│ │
└───────────────────────────────────────────────────────────────┘
Appendix: Protocol Comparison
| Feature | HTTP/1.1 | HTTP/2 | HTTP/3 |
|---|---|---|---|
| Host identification | Host header |
:authority pseudo |
:authority pseudo |
| ModSecurity sees Host | ✅ Yes | ✅ Yes | ❌ No |
| Virtual hosting works | ✅ Yes | ✅ Yes | ✅ Yes |
| CRS Host rules work | ✅ Yes | ✅ Yes | ❌ No |
| Other CRS rules work | ✅ Yes | ✅ Yes | ✅ Yes |
| Custom URI rules work | ✅ Yes | ✅ Yes | ✅ Yes |
Version Compatibility Matrix
| nginx | ModSecurity | Connector | HTTP/3 Host | Status |
|---|---|---|---|---|
| 1.25+ | 3.0.x | 1.0.3 | ❌ | Current stable |
| 1.27+ | 3.0.12+ | 1.0.4 | ❌ | Current |
| 1.28+ | 3.0.14 | 1.0.4 | ❌ | Latest |
| Any | Any | PR #364 | ✅ | Pending merge |
Conclusion
The HTTP/3 Host header issue in ModSecurity is an annoying but manageable problem. The root cause is a disconnect between how HTTP/3 represents host information (:authority pseudo-header) and how the ModSecurity-nginx connector extracts headers.
Key takeaways:
- 🔴 Only 3 rules are affected – 920280, 920290, 920350
- ✅ 99% of security rules work fine – SQLi, XSS, LFI, etc.
- 🛠️ Quick fix available – Remove or conditionally disable affected rules
- 📝 Write future-proof rules – Use REQUEST_URI, not REQUEST_HEADERS:Host
- ⏳ Official fix pending – PR #364 in ModSecurity-nginx connector
The good news: this is a well-understood issue with clear workarounds. The bad news: we’re still waiting for a proper fix in 2026. Such is life in the world of web security.
Stay secure, stay patient. 🛡️
Related Resources:
Leave a Reply