Skip to content

[Bug]: RFC 8446 violation: WolfSSL TLS 1.3 client does not send record_overflow on receipt of an oversized record #10791

Description

@aeyno

Version

wolfssl 5.9.1

Description

A WolfSSL TLS 1.3 client silently rejects a TLS record whose length exceeds the 2^14-byte limit
mandated by RFC 8446 §5.1, without sending a record_overflow alert. The connection is correctly terminated.

OpenSSL correctly terminates the connection with a record_overflow alert for the same input.

RFC 8446 §5.1 states:

"The length MUST NOT exceed 2^14 bytes. An endpoint that receives a record that exceeds this length MUST terminate the connection with a "record_overflow" alert."

Impact

RFC violation.

Reproduction steps

The Python script below acts as a minimal fake TLS server. It reads the client's ClientHello,
then replies with a single TLS ServerHello record whose TLSPlaintext.length field is above the 16,384-byte limit. The ServerHello is otherwise structurally
valid for TLS 1.3: it carries a correct supported_versions extension selecting TLS 1.3 and a
well-formed key_share entry for x25519. The excess length is produced by appending 16
transport_parameters_draft (type 0xffa5) extensions of 1,195 bytes each.

import os, socket, struct

HOST = "127.0.0.1"
PORT = 4433

ALERT_DESC = {
    10: "unexpected_message", 22: "record_overflow", 40: "handshake_failure",
    47: "illegal_parameter",  50: "decode_error",    70: "protocol_version",
}


def ext(t, data):
    return struct.pack(">HH", t, len(data)) + data


def extract_session_id(ch: bytes) -> bytes:
    """Parse legacy_session_id from a raw TLS ClientHello record."""
    # 5 (record hdr) + 4 (hs hdr) + 2 (legacy_version) + 32 (random) = 43
    off = 43
    if len(ch) <= off:
        return b""
    sid_len = ch[off]
    return ch[off + 1: off + 1 + sid_len]


def make_oversized_server_hello(session_id: bytes) -> bytes:
    """
    Build a TLS 1.3 ServerHello with a record length > 2^14 bytes.

    Extensions:
      supported_versions  → TLS 1.3 (0x0304)                     6 B
      key_share           → x25519 (32-byte public value)        40 B
      transport_parameters_draft (0xffa5) × 14, 1195 B each  16730 B
                                                     total:   16776 B

    ServerHello body:  40 B (fixed fields) + 16776 B (extensions) = 16816 B
    Handshake message: 4 B (header) + 16816 B (body)              = 16820 B
    TLSPlaintext.length field:                                       16820 B
    RFC 8446 §5.1 limit:                                             16384 B
    """
    e_sv13 = ext(0x002B, b"\x03\x04")
    # x25519 (0x001d): any 32-byte value is a valid-looking Curve25519 public key
    e_ks   = ext(0x0033, struct.pack(">HH", 0x001d, 32) + os.urandom(32))
    pad    = os.urandom(1191)
    e_pad  = b"".join(ext(0xffa5, pad) for _ in range(16))

    extensions = e_sv13 + e_ks + e_pad
    body = (
        b"\x03\x03"
        + os.urandom(32)
        + bytes([len(session_id)]) + session_id
        + b"\x13\x01"                         # TLS_AES_128_GCM_SHA256
        + b"\x00"                             # compression: null
        + struct.pack(">H", len(extensions)) + extensions
    )
    hs     = b"\x02" + struct.pack(">I", len(body))[1:] + body
    record = b"\x16\x03\x03" + struct.pack(">H", len(hs)) + hs
    print(f"[*] TLSPlaintext.length = {len(hs)} B  (RFC 8446 §5.1 limit: 16384 B)")
    return record


def parse_alerts(data: bytes) -> list[str]:
    msgs, i = [], 0
    while i + 5 <= len(data):
        rec_t = data[i]
        rec_l = struct.unpack(">H", data[i + 3: i + 5])[0]
        body  = data[i + 5: i + 5 + rec_l]
        i    += 5 + rec_l
        if rec_t == 0x15 and len(body) >= 2:
            level = "fatal" if body[0] == 2 else "warning"
            desc  = ALERT_DESC.get(body[1], body[1])
            msgs.append(f"Alert({level},{desc})")
    return msgs


with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as srv:
    srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    srv.bind((HOST, PORT))
    srv.listen(1)
    print(f"[*] Listening on {HOST}:{PORT}")
    conn, addr = srv.accept()
    print(f"[+] Connection from {addr}")
    with conn:
        conn.settimeout(5)
        ch  = conn.recv(65536)
        sid = extract_session_id(ch)
        conn.sendall(make_oversized_server_hello(sid))
        data = b""
        try:
            while True:
                chunk = conn.recv(4096)
                if not chunk:
                    break
                data += chunk
        except socket.timeout:
            pass

alerts = parse_alerts(data)
if not alerts:
    print(f"[!] BUG: client sent no alert (raw: {data[:20].hex() if data else 'empty'})")
elif "record_overflow" in alerts[0]:
    print(f"[+] OK: {alerts[0]}")
else:
    print(f"[!] NOK: {alerts[0]} (expected record_overflow)")

Start the server in one terminal, then connect a TLS 1.3 client in a second terminal:

# Terminal 1
python3 reproducer.py

# Terminal 2 — WolfSSL 5.8.0 client
./build/examples/client/client -h 127.0.0.1 -p 4433 -v 4 -d

Acknowledgements

This bug was found thanks to the tlspuffin fuzzer designed and developed by the tlspuffin team:

  • Nataël Baffou - Engineer, Inria, France
  • Olivier Demengeon - Engineer, Inria, France
  • Tom Gouville - PhD student, Inria, France
  • Lucca Hirschi - Researcher, Inria, France
  • Steve Kremer - Researcher, Inria, France

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions