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:
- It first checks the received
AlertLevel.
- Except for
close_notify, warning-level alerts do not enter the fatal-alert path.
- Warning-level
illegal_parameter and warning-level unknown alerts become
MBEDTLS_ERR_SSL_NON_FATAL.
- 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.
@LiD0209 reported privately:
After further examination, we conclude that this is a non-compliance, but not a security issue.