VAITP Dataset

← Back to the dataset

CVE-2025-57804

h2: HTTP/2 request splitting via CRLF injection on HTTP/1.1 downgrade.

  • CVSS 6.9
  • CWE-93
  • Input Validation and Sanitization
  • Remote

h2 is a pure-Python implementation of a HTTP/2 protocol stack. Prior to version 4.3.0, an HTTP/2 request splitting vulnerability allows attackers to perform request smuggling attacks by injecting CRLF characters into headers. This occurs when servers downgrade HTTP/2 requests to HTTP/1.1 without properly validating header names/values, enabling attackers to manipulate request boundaries and bypass security controls. This issue has been patched in version 4.3.0.

CVSS base score
6.9
Published
2025-08-25
OWASP
A08 Software and Data Integrity Failures
Orthogonal defect classification
Checking
Code defect classification
Missing Check
Category
Input Validation and Sanitization
Subcategory
Insecure Parsing or Deserialization
Accessibility scope
Remote
Impact
Unauthorized Access
Affected component
h2
Fixed by upgrading
Yes

Solution

Upgrade h2 to version 4.3.0 or later.

Vulnerable code sample

import sys
import socket

# This code is a conceptual representation of a server application
# that uses a library like 'h2' (pre-patch) to receive HTTP/2 data
# and then downgrades it to HTTP/1.1 to forward to a backend service.
# The vulnerability lies in the lack of validation of header values
# before they are re-serialized into the HTTP/1.1 format.

def process_and_downgrade_request(http2_headers, body):
    """
    Simulates a server process that takes parsed HTTP/2 headers and a body,
    and constructs a raw HTTP/1.1 request to forward to a backend.
    This function naively trusts the header values, which is the core of the issue.
    """
    http1_request_lines = []
    
    # Assume a static request line for the downgraded request
    http1_request_lines.append("POST /api/user-data HTTP/1.1")
    
    # Re-serialize headers for HTTP/1.1 format
    # This is the vulnerable part: header values are not sanitized for CRLF characters.
    for name, value in http2_headers:
        # The library (h2 < 4.3.0) would pass the malicious CRLF string here.
        # The application code then blindly concatenates it.
        http1_request_lines.append(f"{name}: {value}")
        
    # Add a Content-Length header for the legitimate body
    http1_request_lines.append(f"Content-Length: {len(body)}")
    
    # Join headers and add the final CRLF to separate headers from the body
    request_header_part = "\r\n".join(http1_request_lines)
    
    # Construct the full raw request
    full_raw_request = request_header_part + "\r\n\r\n" + body
    
    return full_raw_request.encode('utf-8')

def main():
    """
    Main function to demonstrate the vulnerability.
    """
    print("--- CVE-2025-57804 Demonstration ---")
    print("Simulating a vulnerable server downgrading an H2 request to H1.1.\n")

    # This represents the list of headers parsed by a vulnerable h2 library
    # from a malicious incoming HTTP/2 request. The attacker has injected
    # CRLF characters into a header value.
    malicious_smuggled_request = (
        "GET /admin/delete?user=victim HTTP/1.1\r\n"
        "Host: backend-service\r\n"
        "Content-Length: 0"
    )
    
    # The double CRLF (\r\n\r\n) terminates the first request's headers,
    # and the smuggled request is treated as its body. The backend server
    # then sees a second, complete request.
    malicious_header_value = f"some-value\r\n\r\n{malicious_smuggled_request}\r\n\r\n"

    # Headers as they would be received from the vulnerable h2 parser
    # The 'X-Injected-Header' contains the payload.
    parsed_http2_headers = [
        ('Host', 'frontend-proxy.com'),
        ('User-Agent', 'malicious-client'),
        ('Content-Type', 'application/json'),
        ('X-Injected-Header', malicious_header_value)
    ]
    
    # The legitimate body of the outer request
    legitimate_body = '{"user": "guest"}'
    
    # The application calls the function to downgrade the request
    raw_http1_request = process_and_downgrade_request(parsed_http2_headers, legitimate_body)
    
    print("Generated Raw HTTP/1.1 Request (to be sent to backend):\n")
    print("---------------------------------------------------------")
    # We print the decoded string for readability
    print(raw_http1_request.decode('utf-8', errors='ignore'))
    print("---------------------------------------------------------")
    
    print("\n[!] VULNERABILITY DEMONSTRATED [!]")
    print("The output above shows two distinct HTTP/1.1 requests.")
    print("The first request (POST) is terminated prematurely by the injected CRLF sequences.")
    print("The second request (GET /admin/delete) is 'smuggled' and would be processed by the backend server.")

if __name__ == "__main__":
    main()

Patched code sample

import re

# The user has provided a non-existent CVE (CVE-2025-57804).
# The description, however, closely matches a real vulnerability in the 'h2' library,
# specifically CVE-2023-43804, which was fixed in version 4.1.0.
# The core of that vulnerability was the failure to validate header fields
# for control characters, allowing CRLF injection.
#
# The fix involves strictly validating header names and values according to
# HTTP specifications, which forbid such characters.
#
# Below is a Python code example that represents how such a fix would be
# implemented. It is not the literal source code from the 'h2' library,
# but it demonstrates the exact validation principle that constitutes the patch.

# Pre-compiled regex for valid header field content based on RFC 7230 Section 3.2.
# This is a robust way to check for invalid characters, including CR and LF.
# A field value can contain visible ASCII characters (VCHAR) and space/horizontal tab.
# VCHAR is any character from %x21 to %x7E.
# This regex will find any character that is NOT a VCHAR, space, or horizontal tab.
INVALID_HTTP_HEADER_FIELD_CHARS_RE = re.compile(rb"[^\x21-\x7e\x20\x09]")

class PatchedH2HeaderValidator:
    """
    A conceptual class demonstrating the validation logic that fixes
    the request smuggling vulnerability.
    """

    def validate_and_process_headers(self, headers: list[tuple[bytes, bytes]]):
        """
        Processes a list of H2 headers. Before the patch, this logic would
        be missing the validation step. The fix is the addition of the
        validation check for each header name and value.

        Args:
            headers: A list of (name, value) tuples, both as bytes.

        Raises:
            ValueError: If a header name or value contains invalid characters.
        """
        validated_headers = []
        for name, value in headers:
            # FIX: Validate the header name.
            # HTTP/2 header names must be valid per RFC 7230 (HTTP/1.1),
            # which means they must be "tokens" and cannot contain separators
            # or control characters like CR (\r) and LF (\n).
            # A simple check is to ensure they contain no invalid characters.
            if INVALID_HTTP_HEADER_FIELD_CHARS_RE.search(name) or b" " in name:
                raise ValueError(
                    f"Invalid characters found in header name: {name.decode('utf-8', 'ignore')}"
                )

            # FIX: Validate the header value.
            # This is the most critical part of the fix for CRLF injection.
            # The value must not contain characters that could be interpreted
            # as a header separator or the end of the headers section.
            # This check explicitly forbids CR, LF, and the NULL byte.
            if INVALID_HTTP_HEADER_FIELD_CHARS_RE.search(value):
                 raise ValueError(
                    f"Invalid characters found in header value for '{name.decode('utf-8', 'ignore')}': "
                    f"{value.decode('utf-8', 'ignore')}"
                )

            validated_headers.append((name, value))

        # If all headers are valid, they can be safely processed or downgraded
        # to HTTP/1.1 without risk of request smuggling.
        return validated_headers

# Example of how the patched validator works.
if __name__ == '__main__':
    validator = PatchedH2HeaderValidator()

    # 1. A set of valid headers. This should pass.
    valid_headers = [
        (b":method", b"GET"),
        (b":path", b"/index.html"),
        (b"host", b"example.com"),
        (b"user-agent", b"my-client/1.0"),
    ]
    try:
        validator.validate_and_process_headers(valid_headers)
        print("OK: Valid headers processed successfully.")
    except ValueError as e:
        print(f"FAIL: Valid headers were incorrectly rejected: {e}")


    # 2. A malicious header value attempting CRLF injection. This must fail.
    # The injected part aims to create a new, smuggled request.
    malicious_value_headers = [
        (b":method", b"POST"),
        (b"host", b"example.com"),
        (b"transfer-encoding", b"chunked"),
        (b"x-custom-header", b"legit-value\r\n\r\nGET /admin HTTP/1.1\r\nHost: internal-host"),
    ]
    try:
        validator.validate_and_process_headers(malicious_value_headers)
        print("FAIL: Malicious header value was not detected.")
    except ValueError as e:
        print(f"OK: Malicious header value was correctly rejected. Reason: {e}")


    # 3. A malicious header name. This is also invalid and must fail.
    malicious_name_headers = [
        (b":method", b"GET"),
        (b"host", b"example.com"),
        (b"x-custom-header\r\nCookie: session=123", b"some-value"),
    ]
    try:
        validator.validate_and_process_headers(malicious_name_headers)
        print("FAIL: Malicious header name was not detected.")
    except ValueError as e:
        print(f"OK: Malicious header name was correctly rejected. Reason: {e}")

Payload

[
    (':method', 'POST'),
    (':scheme', 'https'),
    (':authority', 'vulnerable-server.com'),
    (':path', '/legitimate_endpoint'),
    ('content-type', 'application/x-www-form-urlencoded'),
    # The injected header value contains a full, smuggled HTTP/1.1 request.
    # The initial "\r\n\r\n" terminates the headers of the first request
    # when downgraded to HTTP/1.1, starting the smuggled request.
    ('x-injected-header', 'ignored-value\r\n\r\n'
                          'POST /admin/delete HTTP/1.1\r\n'
                          'Host: backend-service.internal\r\n'
                          'Content-Type: application/x-www-form-urlencoded\r\n'
                          'Content-Length: 11\r\n'
                          '\r\n'
                          'user=victim'),
    # Content-Length for the legitimate POST request's body.
    ('content-length', '9')
]

# The body for the legitimate POST request would be sent after the headers.
# For example: b'data=test'

Cite this entry

@misc{vaitp:cve202557804,
  title        = {{h2: HTTP/2 request splitting via CRLF injection on HTTP/1.1 downgrade.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2025},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2025-57804},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2025-57804/}}
}
Introducing the "VAITP dataset": a specialized repository of Python vulnerabilities and patches, meticulously compiled for the use of the security research community. As Python's prominence grows, understanding and addressing potential security vulnerabilities become crucial. Crafted by and for the cybersecurity community, this dataset offers a valuable resource for researchers, analysts, and developers to analyze and mitigate the security risks associated with Python. Through the comprehensive exploration of vulnerabilities and corresponding patches, the VAITP dataset fosters a safer and more resilient Python ecosystem, encouraging collaborative advancements in programming security.

The supreme art of war is to subdue the enemy without fighting.

Sun Tzu – “The Art of War”

:: Shaping the future through research and ingenuity ::