ModSecurity, HTTP/3, and the Missing Host Header

Or: Why Your WAF Rules Work on HTTP/2 But Break on QUIC

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 :authorityHost 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:

  1. Modern browsers always send :authority (just not visible to ModSecurity)
  2. nginx still routes correctly based on :authority
  3. Most attacks don’t rely on manipulating the Host header
  4. 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

Your email address will not be published. Required fields are marked *