VAITP Dataset

← Back to the dataset

CVE-2025-69225

AIOHTTP allows non-ASCII decimals in Range header, risking request smuggling.

  • CVSS 2.7
  • CWE-444
  • Input Validation and Sanitization
  • Remote

AIOHTTP is an asynchronous HTTP client/server framework for asyncio and Python. Versions 3.13.2 and below contain parser logic which allows non-ASCII decimals to be present in the Range header. There is no known impact, but there is the possibility that there's a method to exploit a request smuggling vulnerability. This issue is fixed in version 3.13.3.

CVSS base score
2.7
Published
2026-01-06
OWASP
A04 Insecure Design
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
AIOHTTP
Fixed by upgrading
Yes

Solution

Upgrade AIOHTTP to version 3.13.3.

Vulnerable code sample

import asyncio
from aiohttp import web, ClientSession

# This code demonstrates the logic of the vulnerability described in
# CVE-2025-69225. A vulnerable aiohttp server (version 3.13.2 and below)
# would incorrectly parse a 'Range' header containing non-ASCII decimals.
# This script sets up a server and a client to test this behavior.

async def vulnerable_range_handler(request):
    """
    This server handler inspects the incoming request's 'Range' header.
    
    The vulnerability is in the underlying parser that populates the
    `request.http_range` attribute. A vulnerable version will successfully
    parse non-ASCII digits, while a patched version will ignore them.
    """
    range_header_value = request.headers.get('Range', 'Not present')
    
    # The `http_range` attribute holds a slice object if parsing is successful.
    parsed_range = request.http_range
    
    # The response text shows whether the non-ASCII range was parsed.
    # On a vulnerable system, 'start' and 'stop' would be integers.
    # On a patched system, they would be 'None'.
    response_text = (
        f"Raw 'Range' header received: {range_header_value}\n"
        f"Parsed http_range.start: {parsed_range.start}\n"
        f"Parsed http_range.stop: {parsed_range.stop}\n"
    )
    
    if parsed_range.start is not None:
        response_text += "\nRESULT: VULNERABLE. Non-ASCII range was parsed."
    else:
        response_text += "\nRESULT: NOT VULNERABLE. Non-ASCII range was ignored."

    return web.Response(text=response_text)

async def main():
    # --- 1. Server Setup ---
    app = web.Application()
    app.router.add_get('/', vulnerable_range_handler)
    runner = web.AppRunner(app)
    await runner.setup()
    site = web.TCPSite(runner, '127.0.0.1', 8080)
    await site.start()
    
    server_address = 'http://127.0.0.1:8080'
    print(f"Server running at {server_address}")

    # --- 2. Client Request ---
    # The client sends a 'Range' header using non-ASCII full-width digits
    # (e.g., U+FF11 for '1' and U+FF19 for '9').
    # This is the vector used to demonstrate the parsing flaw.
    vulnerable_headers = {'Range': 'bytes=1-99'}
    
    print("\nSending request with malicious 'Range' header...")
    print(f"-> Header: Range: {vulnerable_headers['Range']}")
    
    try:
        async with ClientSession() as session:
            async with session.get(server_address, headers=vulnerable_headers) as response:
                print("\n--- Server Response ---")
                server_output = await response.text()
                print(server_output)
    except Exception as e:
        print(f"\nAn error occurred during client request: {e}")
    finally:
        # --- 3. Server Shutdown ---
        await runner.cleanup()
        print("\nServer shut down.")

if __name__ == "__main__":
    # To run this demonstration, you need to have 'aiohttp' installed.
    # `pip install aiohttp`
    # This script simulates the environment before the fix for CVE-2025-69225.
    # Running it on a vulnerable version would show integer values for start/stop
    # and a "VULNERABLE" result.
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        print("\nDemonstration stopped by user.")

Patched code sample

import re

class InvalidRangeHeader(ValueError):
    """Custom exception for malformed Range headers."""
    pass

# The vulnerability was that the parser allowed non-ASCII decimals in the Range header.
# The fix is to strictly enforce ASCII digits '0'-'9' as specified by HTTP RFCs.
# This regex uses `[0-9]` to explicitly match only ASCII digits, which is
# the core of the fix. A vulnerable parser might have used a method that was
# Unicode-aware, thus incorrectly accepting characters like '1', '2', etc.
_FIXED_RANGE_HEADER_RE = re.compile(r"bytes=([0-9]+)?-([0-9]+)?")

def parse_range_header_with_fix(header_value: str) -> tuple[int | None, int | None]:
    """
    Parses a Range header value, demonstrating the fix for CVE-2025-69225.
    
    This function represents the patched logic. It rejects headers containing
    non-ASCII digits (e.g., "bytes=1-10") by using a regular expression that
    only matches ASCII digits [0-9]. A vulnerable version would have accepted
    and parsed such a header.
    """
    if not isinstance(header_value, str):
        raise InvalidRangeHeader("Header value must be a string.")

    # Using fullmatch ensures the entire string conforms to the pattern.
    match = _FIXED_RANGE_HEADER_RE.fullmatch(header_value.strip())
    
    if not match:
        # Because the regex is strict on using only ASCII digits [0-9], this check
        # now correctly fails for headers with non-ASCII numeric characters.
        # This is the behavior that fixes the vulnerability.
        raise InvalidRangeHeader(
            f"Invalid Range header format or non-ASCII digits detected in '{header_value}'"
        )

    start_str, end_str = match.groups()

    # The regex guarantees that if start_str or end_str are not None, they
    # contain only ASCII digits, so int() conversion is safe.
    start = int(start_str) if start_str is not None else None
    end = int(end_str) if end_str is not None else None

    if start is None and end is None:
         raise InvalidRangeHeader(
             "Range header must specify at least a start or an end position."
         )

    if start is not None and end is not None and start > end:
        raise InvalidRangeHeader(f"Invalid range: start ({start}) cannot be greater than end ({end}).")

    return start, end

Payload

GET /some/file HTTP/1.1
Host: vulnerable.example.com
Range: bytes=0-100

Cite this entry

@misc{vaitp:cve202569225,
  title        = {{AIOHTTP allows non-ASCII decimals in Range header, risking request smuggling.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2025-69225},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2025-69225/}}
}
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 ::