Skip to content

ClientHello supported_versions Accepts a Trailing Byte #10739

@LiD0209

Description

@LiD0209

ClientHello supported_versions Accepts a Trailing Byte

1. Summary

mbedTLS accepts a malformed TLS 1.3 ClientHello where the
supported_versions extension contains a valid ProtocolVersion followed by
one trailing byte.

The malformed payload tested was logically:

supported_versions.extension_data = 03 03 04 00
                                  |  |-----| |
                                  |  TLS 1.3 |
                                  |          trailing byte
                                  versions_len = 3

This should be rejected as a syntax error because the ClientHello form of
SupportedVersions is a vector of 2-byte ProtocolVersion elements. A vector
length of 3 is not an exact multiple of the element size.

Runtime testing shows that mbedTLS still negotiates TLS 1.3 and sends a
ServerHello instead of terminating the handshake with decode_error.

2. Specification Description

2.1 ProtocolVersion is 2 bytes

From RFC 8446, Section 3.4:

uint16 ProtocolVersion;

So each ProtocolVersion element occupies exactly 2 bytes.

2.2 ClientHello supported_versions is a ProtocolVersion vector

From RFC 8446, Section 4.2.1, the ClientHello arm of SupportedVersions is:

ProtocolVersion versions<2..254>;

This means the extension body is not arbitrary bytes. It is a TLS vector whose
elements are ProtocolVersion values.

2.3 Vector length must match the element size

From the TLS presentation language rules in RFC 8446, Section 3.4:

must be an exact multiple of the length of a single element

Because the element is ProtocolVersion and ProtocolVersion is 2 bytes, the
versions vector length must be even. A length of 3 is therefore syntactically
invalid.

2.4 Syntax parse failures are decode_error

From RFC 8446, Section 6.2:

MUST terminate the connection with a "decode_error" alert

Therefore, a ClientHello containing ProtocolVersion versions<2..254> with an
odd byte length should be rejected with decode_error.

3. mbedTLS Code Description

The relevant parser is:

mbedtls-development/library/ssl_tls13_server.c:773
ssl_tls13_parse_supported_versions_ext()

The important control flow is at
mbedtls-development/library/ssl_tls13_server.c:783-804:

versions_len = p[0];
...
while (p < versions_end) {
    read 2-byte ProtocolVersion
    ...
    if TLS 1.3 is found:
        found_supported_version = 1
        break
}

The code checks that versions_len bytes are inside the extension boundary, and
each loop iteration checks that 2 bytes are available before reading a
ProtocolVersion.

However, once TLS 1.3 or TLS 1.2 is found, the loop exits immediately. The
parser does not verify either of these conditions:

  • versions_len % 2 == 0
  • p == versions_end after parsing

As a result, if the first two bytes contain 0x0304 and one extra byte follows,
the parser accepts the extension before it ever reaches the malformed trailing
byte.

4. Runtime Reproduction

The reproduction script is:

m601-700/repro_id637_supported_versions_tail.py

The script starts a real mbedTLS TLS 1.3 server and client, then uses a small TCP
proxy to mutate only the ClientHello supported_versions extension.

The mutation is implemented at
m601-700/repro_id637_supported_versions_tail.py:70-84:

original_versions_len: 2
mutated_versions_len: 3
inserted_byte: 00

Run:

python .\m601-700\repro_id637_supported_versions_tail.py

Observed result:

[mutated]
mutated: True
mutation: {'original_versions_len': 2, 'mutated_versions_len': 3, 'inserted_byte': '00'}
server_first_record_type: 22
server_first_record_hex: 160303007a020000760303...
client_returncode: 1

server_first_record_type: 22 means the server sent a TLS Handshake record. If
the malformed ClientHello had been rejected immediately, the first response
should have been an Alert record, type 21.

The server log confirms acceptance:

m601-700/id637_mutated_server.log:69
Negotiated version: [0304]

m601-700/id637_mutated_server.log:168
handshake state: CLIENT_HELLO -> SERVER_HELLO

m601-700/id637_mutated_server.log:233
output record: msgtype = 22

The client later fails with a MAC verification error. That is expected because
the proxy mutates the ClientHello seen by the server, while the client computes
the TLS 1.3 transcript using its original ClientHello. This later failure does
not change the server-side finding: the malformed supported_versions vector
was already accepted and processed.

5. Inconsistency

The specification requires the ClientHello supported_versions extension to be
parsed as:

ProtocolVersion versions<2..254>

Since ProtocolVersion is uint16, the vector body must consist of complete
2-byte elements. A vector body with length 3 does not conform to the TLS
presentation syntax and should trigger decode_error.

mbedTLS instead accepts the malformed vector if a supported version appears
before the trailing byte. The immediate cause is the early break after finding
TLS 1.3/TLS 1.2, combined with the absence of an exact-consumption or
element-size-multiple check.

In short:

Expected behavior:
  reject versions_len = 3 with decode_error

Observed mbedTLS behavior:
  accept 03 03 04 00, negotiate TLS 1.3, send ServerHello

6. Suggested Fix Direction

The parser should reject malformed vector lengths before scanning versions, for
example:

if versions_len < 2 or versions_len > 254 or versions_len % 2 != 0:
    fail with decode_error

Alternatively, the parser can require exact consumption after scanning the
vector. The clearer fix is to validate the vector length modulo the
ProtocolVersion element size before entering the loop.

Metadata

Metadata

Assignees

No one assigned

    Type

    No fields configured for Bug.

    Projects

    Status
    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions