Commit 689fb0a384 for openssl.org
commit 689fb0a3847076199289b9099f9032e5045e23dd
Author: Mounir IDRASSI <mounir.idrassi@idrix.fr>
Date: Mon Aug 31 20:45:05 2026 +0900
Fix DTLS 1.3 accepting unauthenticated plaintext alerts
DTLS 1.3 accepted forged DTLSPlaintext records after traffic keys
were installed. Injected alerts could terminate or desynchronise an
association, while plaintext handshake and ACK records could invoke
WPACKET_cleanup() on an uninitialised WPACKET.
Silently discard DTLSPlaintext in nonzero epochs, as required by RFC
9147, after consuming the complete fragment. Harden tls13_cipher() by
honouring allow_plain_alerts, rejecting non-unified DTLS headers, and
ensuring WPACKET is initialised before cleanup.
Keep validate_record_header unset because invalid DTLS records must be
silently discarded rather than converted into fatal alerts.
Assisted-by: pi:kimi-k3
Reviewed-by: Ryan Hooper <ryanh@openssl.foundation>
Reviewed-by: Frederik Wedel-Heinen <fwh.openssl@gmail.com>
Merge-date: Mon Sep 7 14:41:16 2026
Merged-from: https://github.com/openssl/openssl/pull/32622
diff --git a/ssl/record/methods/dtls_meth.c b/ssl/record/methods/dtls_meth.c
index ed176773e4..31e8d6b775 100644
--- a/ssl/record/methods/dtls_meth.c
+++ b/ssl/record/methods/dtls_meth.c
@@ -705,6 +705,18 @@ again:
/* set state for later operations */
rl->rstate = SSL_ST_READ_HEADER;
+ /*
+ * RFC 9147 permits DTLSPlaintext only in epoch 0. Silently discard it
+ * after reading the fragment so later records remain framed.
+ */
+ if (rl->version == DTLS1_3_VERSION
+ && rl->epoch != 0
+ && !DTLS13_UNI_HDR_FIX_BITS_IS_SET(rr->type)) {
+ rr->length = 0;
+ rl->packet_length = 0;
+ goto again;
+ }
+
/*
* rfc9147:
* This procedure requires the ciphertext length to be at least 16 bytes.
diff --git a/ssl/record/methods/tls13_meth.c b/ssl/record/methods/tls13_meth.c
index 13228d02d9..c931fe0810 100644
--- a/ssl/record/methods/tls13_meth.c
+++ b/ssl/record/methods/tls13_meth.c
@@ -144,17 +144,21 @@ static int tls13_cipher(OSSL_RECORD_LAYER *rl, TLS_RL_RECORD *recs,
}
/*
- * If we're sending an alert and ctx != NULL then we must be forcing
- * plaintext alerts. If we're reading and ctx != NULL then we allow
- * plaintext alerts at certain points in the handshake. If we've got this
- * far then we have already validated that a plaintext alert is ok here.
+ * Plaintext alerts are allowed only when explicitly enabled. DTLS needs
+ * this check here because it does not run the TLS header validator.
*/
if (rec->type == SSL3_RT_ALERT) {
+ if (!rl->allow_plain_alerts)
+ return 0;
memmove(rec->data, rec->input, rec->length);
rec->input = rec->data;
return 1;
}
+ /* Keyed DTLS 1.3 layers accept only unified headers. */
+ if (isdtls && !DTLS13_UNI_HDR_FIX_BITS_IS_SET(rec->type))
+ return 0;
+
/* For integrity-only ciphers, nonce_len is same as MAC size */
if (rl->mac_ctx != NULL) {
nonce_len = EVP_MAC_CTX_get_mac_size(rl->mac_ctx);
@@ -223,8 +227,16 @@ static int tls13_cipher(OSSL_RECORD_LAYER *rl, TLS_RL_RECORD *recs,
addlen = 1;
}
- if ((isdtls && !ossl_assert(!DTLS13_UNI_HDR_CID_BIT_IS_SET(rec->type)))
- || !WPACKET_init_static_len(&wpkt, recheader, sizeof(recheader), 0)
+ /*
+ * Reject unsupported CID before WPACKET setup so cleanup cannot touch
+ * an uninitialised packet.
+ */
+ if (isdtls && !ossl_assert(!DTLS13_UNI_HDR_CID_BIT_IS_SET(rec->type))) {
+ RLAYERfatal(rl, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+ return 0;
+ }
+
+ if (!WPACKET_init_static_len(&wpkt, recheader, sizeof(recheader), 0)
|| !WPACKET_put_bytes_u8(&wpkt, rec->type)
|| (isdtls
&& (sbit ? !WPACKET_put_bytes_u16(&wpkt, seqnum)
@@ -520,6 +532,10 @@ const struct record_functions_st dtls_1_3_funcs = {
tls_default_set_protocol_version,
tls_default_read_n,
dtls_get_more_records,
+ /*
+ * Keep this NULL: the TLS validator raises fatal errors, while DTLS must
+ * silently discard invalid records (RFC 9147 section 4.5.2).
+ */
NULL,
tls13_post_process_record,
NULL,