Skip to content

TLS 1.3 warning-level error alerts are treated as non-fatal #10924

Description

@gilles-peskine-arm

@LiD0209 reported privately:

I would like to report a potential TLS 1.3 standards-compliance and security issue in the handling of received alerts.

In my testing, TLS 1.3 alerts that must be treated as error alerts—such as illegal_parameter(47) and unknown alert types—are handled as non-fatal when they are encoded with AlertLevel=warning. A fatal-level illegal_parameter is correctly reported as a fatal alert, but changing only the alert level to warning causes the same alert description to be downgraded to EOF, timeout, or continued connection processing.

A focused follow-up test also showed that bidirectional application data could still be exchanged after receiving a warning-level illegal_parameter or an unknown warning-level alert.

This behavior appears inconsistent with RFC 9846, Sections 6 and 6.2, as well as the equivalent requirements in RFC 8446. These specifications require Section 6.2 alerts and unknown alert types to be treated as error alerts regardless of the AlertLevel value received on the wire.

TLS 1.3 Section 6.2 Alerts Encoded with AlertLevel=warning Are Downgraded to Non-Fatal Handling

Summary

Scope clarification:

  • This finding is not about every warning-level alert.
  • It is specifically about TLS 1.3 alerts that the receiver must still treat as error alerts:
    Section 6.2 alerts such as illegal_parameter(47), and unknown alert types.

Key finding:

  • fatal illegal_parameter is surfaced as a fatal alert.
  • The same illegal_parameter(47) description encoded with AlertLevel=warning is downgraded into EOF or timeout behavior.
  • A warning-level unknown alert is also downgraded into EOF behavior.
  • A focused follow-up probe shows that warning-level illegal_parameter and warning-level unknown alerts can be followed by continued bidirectional application-data exchange.

Standard Requirement

Normative text:

All the alerts listed in Section 6.2 MUST be sent with
AlertLevel=fatal and MUST be treated as error alerts when received
regardless of the AlertLevel in the message.
Unknown Alert types MUST be treated as error alerts.

Relevant surrounding rule:

Upon receiving an error alert, the TLS implementation SHOULD indicate
an error to the application and MUST NOT allow any further data to be
sent or received on the connection.

Important exceptions do not cover this case:

  • close_notify is a closure alert with separate shutdown semantics.
  • user_canceled is special in RFC 9846 and may be followed by continued reading until close_notify.
  • Neither exception applies to illegal_parameter(47) or unknown alert types.

Relevant Source Code

library/ssl_msg.c:4970-5009

if (ssl->in_msgtype == MBEDTLS_SSL_MSG_ALERT) {
    ...
    if (ssl->in_msg[0] == MBEDTLS_SSL_ALERT_LEVEL_FATAL) {
        ssl->in_fatal_alert_recv = 1;
        ssl->in_fatal_alert_type = ssl->in_msg[1];
        return MBEDTLS_ERR_SSL_FATAL_ALERT_MESSAGE;
    }

    if (ssl->in_msg[0] == MBEDTLS_SSL_ALERT_LEVEL_WARNING &&
        ssl->in_msg[1] == MBEDTLS_SSL_ALERT_MSG_CLOSE_NOTIFY) {
        return MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY;
    }

#if defined(MBEDTLS_SSL_RENEGOTIATION_ENABLED)
    if (ssl->in_msg[0] == MBEDTLS_SSL_ALERT_LEVEL_WARNING &&
        ssl->in_msg[1] == MBEDTLS_SSL_ALERT_MSG_NO_RENEGOTIATION) {
        return 0;
    }
#endif
    return MBEDTLS_ERR_SSL_NON_FATAL;
}

This receive path makes the primary decision from the received AlertLevel byte.
Only AlertLevel=fatal enters the fatal-alert path.
Warning-level illegal_parameter and warning-level unknown alerts fall through to
MBEDTLS_ERR_SSL_NON_FATAL.

library/ssl_msg.c:4019-4096

/*
 * Silently ignore non-fatal alert ... and continue reading
 * until a valid record is found.
 */
int mbedtls_ssl_read_record(mbedtls_ssl_context *ssl,
                            unsigned update_hs_digest)
{
    ...
    } while (MBEDTLS_ERR_SSL_NON_FATAL           == ret  ||
             MBEDTLS_ERR_SSL_CONTINUE_PROCESSING == ret);
    ...
}

Once the alert handler returns MBEDTLS_ERR_SSL_NON_FATAL, the record-reading path
keeps waiting for more peer input instead of aborting on an error alert.

library/ssl_msg.c:5785-5788

/* Fatal and closure alerts handled by mbedtls_ssl_read_record() */
if (ssl->in_msgtype == MBEDTLS_SSL_MSG_ALERT) {
    MBEDTLS_SSL_DEBUG_MSG(2, ("ignoring non-fatal non-closure alert"));
    return MBEDTLS_ERR_SSL_WANT_READ;
}

There is no later TLS 1.3-specific override that upgrades these warning-level
Section 6.2 alerts back into error alerts. Later read paths continue to treat
them as non-fatal.

Why This Violates the Standard

The standard requires the receiver to classify these TLS 1.3 cases from the
alert description, not from the received AlertLevel byte.

The implementation does the opposite:

  1. It first checks the received AlertLevel.
  2. Except for close_notify, warning-level alerts do not enter the fatal-alert path.
  3. Warning-level illegal_parameter and warning-level unknown alerts become
    MBEDTLS_ERR_SSL_NON_FATAL.
  4. The record-reading path then keeps reading, and application paths can still
    treat the connection as readable.

That behavior is inconsistent with RFC 9846 and RFC 8446.

Runtime Evidence

Fresh Rerun on 2026-08-04

This report includes a fresh rerun performed on August 4, 2026. The probe sent
the same alert description at fatal and warning levels, tested an unknown
warning alert, held the connection open after a warning, and attempted
application-data exchange after warning alerts.

Case Wire alert bytes Observed result Why it matters
fatal-illegal-parameter-close 15 03 03 00 02 02 2f -0x7780 fatal alert Positive control
warning-illegal-parameter-close 15 03 03 00 02 01 2f -0x7280 EOF Same description is downgraded when only the level changes
warning-unknown-close 15 03 03 00 02 01 fa -0x7280 EOF Unknown alert is not treated as an error alert
warning-illegal-parameter-hold 15 03 03 00 02 01 2f -0x6800 timeout after alert parsing Client keeps waiting after parsing the alert
warning-followup 15 03 03 00 02 01 2f and 15 03 03 00 02 01 fa connection_continued=true, roundtrip_after_alert=true Post-alert application data still flows

Selected Excerpts

The fatal control returned:

! mbedtls_ssl_handshake returned -0x7780
Last error was: -0x7780 - SSL - A fatal alert message was received from our peer

The warning-level illegal_parameter close case returned:

! mbedtls_ssl_handshake returned -0x7280
Last error was: -0x7280 - SSL - The connection indicated an EOF

The unknown-warning close case returned:

! mbedtls_ssl_handshake returned -0x7280
Last error was: -0x7280 - SSL - The connection indicated an EOF

When the sender held the connection open after warning illegal_parameter, the
client returned:

ssl_msg.c:4980: |2| got an alert message, type: [1:47]
ssl_msg.c:2028: |2| ssl->f_recv(_timeout)() returned -26624 (-0x6800)
ssl_msg.c:4723: |1| mbedtls_ssl_fetch_input() returned -26624 (-0x6800)
! mbedtls_ssl_handshake returned -0x6800
Last error was: -0x6800 - SSL - The operation timed out

When the sender exchanged data after warning alerts, the probe reported:

{"label":"warning_illegal_parameter",...,"connection_continued":true,"roundtrip_after_alert":true,...}
{"label":"warning_unknown",...,"connection_continued":true,"roundtrip_after_alert":true,...}

The hold-open and follow-up variants are the strongest evidence:

  • The hold-open case shows that warning-level illegal_parameter is not merely
    mapped to a different immediate error code. After parsing the alert, the
    client keeps waiting for more input until the read times out.
  • The follow-up case shows that warning-level illegal_parameter and
    warning-level unknown alerts are treated as non-fatal strongly enough that
    post-alert application data can still be exchanged in both directions.

Impact

  • Protocol errors are surfaced to the application as EOF or timeout instead of as
    explicit alert-driven failure.
  • Unknown alert types are not reliably classified as error alerts.
  • A TLS 1.3 connection may keep waiting for more network input after receiving an
    alert that should have caused abortive closure.
  • In focused follow-up testing, post-alert application data can still flow after
    warning-level illegal_parameter and warning-level unknown alerts.

This makes diagnosis harder and weakens the distinction between ordinary transport
shutdown and protocol failure.

Fix Direction

  • In the TLS 1.3 receive path, do not classify these alerts only from the received
    AlertLevel.
  • For alerts listed in Section 6.2, and for unknown alert types, enter the
    error-alert path regardless of the received AlertLevel.
  • Preserve only the alert-specific exceptions that the standard explicitly treats
    differently, such as close_notify and user_canceled.

Decision Reason

The standard, the code, and the fresh rerun all point to the same conclusion:

  • RFC 9846 and RFC 8446 require these alerts to be treated as error alerts on receipt.
  • The mbedTLS receive path downgrades warning-level variants to
    MBEDTLS_ERR_SSL_NON_FATAL.
  • The August 4, 2026 rerun reproduced fatal-versus-warning divergence for the
    same alert description.
  • The hold-open rerun showed continued reading after warning-level
    illegal_parameter.
  • The follow-up rerun showed that warning-level illegal_parameter and
    warning-level unknown alerts can still be followed by successful bidirectional
    application-data exchange.

This should remain issue_found.

Consolidation Record

This canonical report also absorbs requirements req-2f50b9dfa86702bedd7d, req-51d45467fea2cce21250, req-ad6d95cc35fbdfc6fd34, and req-6869afa16f814d1695ed. The former handshake_failure(40) report is retained here as a Section 6.2 alert-description subcase of the same AlertLevel=warning downgrade root cause.

After further examination, we conclude that this is a non-compliance, but not a security issue.

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    Status
    Incoming

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions