From 2d01b1b1c068dbc5277797b7ed56a7cd14837789 Mon Sep 17 00:00:00 2001 From: starlet-dx <15929766099@163.com> Date: Mon, 14 Oct 2024 15:35:14 +0800 Subject: [PATCH] Fix CVE-2024-49214 --- CVE-2024-49214.patch | 458 +++++++++++++++++++++++++++++++++++++++++++ haproxy.spec | 6 +- 2 files changed, 463 insertions(+), 1 deletion(-) create mode 100644 CVE-2024-49214.patch diff --git a/CVE-2024-49214.patch b/CVE-2024-49214.patch new file mode 100644 index 0000000..e4160ee --- /dev/null +++ b/CVE-2024-49214.patch @@ -0,0 +1,458 @@ +From fe5685af820ae62fe5b0d80b5ed7a2ffc41a036f Mon Sep 17 00:00:00 2001 +From: Frederic Lecaille +Date: Fri, 30 Aug 2024 15:38:54 +0200 +Subject: [PATCH] BUG/MEDIUM: quic: always validate sender address on 0-RTT + +It has been reported by Wedl Michael, a student at the University of Applied +Sciences St. Poelten, a potential vulnerability into haproxy as described below. + +An attacker could have obtained a TLS session ticket after having established +a connection to an haproxy QUIC listener, using its real IP address. The +attacker has not even to send a application level request (HTTP3). Then +the attacker could open a 0-RTT session with a spoofed IP address +trusted by the QUIC listen to bypass IP allow/block list and send HTTP3 requests. + +To mitigate this vulnerability, one decided to use a token which can be provided +to the client each time it successfully managed to connect to haproxy. These +tokens may be reused for future connections to validate the address/path of the +remote peer as this is done with the Retry token which is used for the current +connection, not the next one. Such tokens are transported by NEW_TOKEN frames +which was not used at this time by haproxy. + +So, each time a client connect to an haproxy QUIC listener with 0-RTT +enabled, it is provided with such a token which can be reused for the +next 0-RTT session. If no such a token is presented by the client, +haproxy checks if the session is a 0-RTT one, so with early-data presented +by the client. Contrary to the Retry token, the decision to refuse the +connection is made only when the TLS stack has been provided with +enough early-data from the Initial ClientHello TLS message and when +these data have been accepted. Hopefully, this event arrives fast enough +to allow haproxy to kill the connection if some early-data have been accepted +without token presented by the client. + +quic_build_post_handshake_frames() has been modified to build a NEW_TOKEN +frame with this newly implemented token to be transported inside. + +quic_tls_derive_retry_token_secret() was renamed to quic_do_tls_derive_token_secre() +and modified to be reused and derive the secret for the new token implementation. + +quic_token_validate() has been implemented to validate both the Retry and +the new token implemented by this patch. When this is a non-retry token +which could not be validated, the datagram received is marked as requiring +a Retry packet to be sent, and no connection is created. + +When the Initial packet does not embed any non-retry token and if 0-RTT is enabled +the connection is marked with this new flag: QUIC_FL_CONN_NO_TOKEN_RCVD. As soon +as the TLS stack detects that some early-data have been provided and accepted by +the client, the connection is marked to be killed (QUIC_FL_CONN_TO_KILL) from +ha_quic_add_handshake_data(). This is done calling qc_ssl_eary_data_accepted() +new function. The secret TLS handshake is interrupted as soon as possible returnin +0 from ha_quic_add_handshake_data(). The connection is also marked as +requiring a Retry packet to be sent (QUIC_FL_CONN_SEND_RETRY) from +ha_quic_add_handshake_data(). The the handshake I/O handler (quic_conn_io_cb()) +knows how to behave: kill the connection after having sent a Retry packet. + +About TLS stack compatibility, this patch is supported by aws-lc. It is +disabled for wolfssl which does not support 0-RTT at this time thanks +to HAVE_SSL_0RTT_QUIC. + +This patch depends on these commits: + + MINOR: quic: Add trace for QUIC_EV_CONN_IO_CB event. + MINOR: quic: Implement qc_ssl_eary_data_accepted(). + MINOR: quic: Modify NEW_TOKEN frame structure (qf_new_token struct) + BUG/MINOR: quic: Missing incrementation in NEW_TOKEN frame builder + MINOR: quic: Token for future connections implementation. + MINOR: quic: Implement quic_tls_derive_token_secret(). + MINOR: tools: Implement ipaddrcpy(). + +Must be backported as far as 2.6. + +(cherry picked from commit f627b9272bd8ffca6f2f898bfafc6bf0b84b7d46) +[fl: Add ->flags to quic_dgram struct (would arrive with quic_initial feature). + Add QUIC_DGRAM_FL_ quic_dgram flags (would arrive with quic_initial feature). + Modify quic_rx_pkt_retrieve_conn() to fix a compilation issue and correctly + handle the "if (pkt->token_len) {}" else block to do so with quic_initial + feature] +Signed-off-by: Frederic Lecaille +(cherry picked from commit e875aa59a9216d42639b802b5008afc733e4c940) +[wt: move QUIC_CONN_FL_* upper in quic_conn-t.h; ctx adj in quic_dgram; + include quic_cid-t for struct quic_cid in quic_rx-t.h] +Signed-off-by: Willy Tarreau +--- + include/haproxy/quic_conn-t.h | 3 ++ + include/haproxy/quic_rx-t.h | 2 ++ + include/haproxy/quic_sock-t.h | 5 +++ + src/quic_conn.c | 65 ++++++++++++++++++++++++++++++--- + src/quic_retry.c | 8 +---- + src/quic_rx.c | 80 +++++++++++++++++++++++++++++++++++------ + src/quic_sock.c | 2 ++ + src/quic_ssl.c | 20 ++++++++++- + 8 files changed, 161 insertions(+), 24 deletions(-) + +diff --git a/include/haproxy/quic_conn-t.h b/include/haproxy/quic_conn-t.h +index a126e04..382454c 100644 +--- a/include/haproxy/quic_conn-t.h ++++ b/include/haproxy/quic_conn-t.h +@@ -291,6 +291,9 @@ struct quic_conn_cntrs { + #define QUIC_FL_CONN_IPKTNS_DCD (1U << 15) /* Initial packet number space discarded */ + #define QUIC_FL_CONN_HPKTNS_DCD (1U << 16) /* Handshake packet number space discarded */ + #define QUIC_FL_CONN_PEER_VALIDATED_ADDR (1U << 17) /* Peer address is considered as validated for this connection. */ ++#define QUIC_FL_CONN_NO_TOKEN_RCVD (1U << 18) /* Client dit not send any token */ ++#define QUIC_FL_CONN_SEND_RETRY (1U << 19) /* A send retry packet must be sent */ ++/* gap here */ + #define QUIC_FL_CONN_TO_KILL (1U << 24) /* Unusable connection, to be killed */ + #define QUIC_FL_CONN_TX_TP_RECEIVED (1U << 25) /* Peer transport parameters have been received (used for the transmitting part) */ + #define QUIC_FL_CONN_FINALIZED (1U << 26) /* QUIC connection finalized (functional, ready to send/receive) */ +diff --git a/include/haproxy/quic_rx-t.h b/include/haproxy/quic_rx-t.h +index 9ef8e7a..e77755b 100644 +--- a/include/haproxy/quic_rx-t.h ++++ b/include/haproxy/quic_rx-t.h +@@ -1,6 +1,8 @@ + #ifndef _HAPROXY_RX_T_H + #define _HAPROXY_RX_T_H + ++#include ++ + extern struct pool_head *pool_head_quic_conn_rxbuf; + extern struct pool_head *pool_head_quic_dgram; + extern struct pool_head *pool_head_quic_rx_packet; +diff --git a/include/haproxy/quic_sock-t.h b/include/haproxy/quic_sock-t.h +index 67a5749..83ab32f 100644 +--- a/include/haproxy/quic_sock-t.h ++++ b/include/haproxy/quic_sock-t.h +@@ -25,6 +25,9 @@ struct quic_receiver_buf { + struct mt_list rxbuf_el; /* list element into receiver.rxbuf_list. */ + }; + ++#define QUIC_DGRAM_FL_REJECT 0x00000001 ++#define QUIC_DGRAM_FL_SEND_RETRY 0x00000002 ++ + /* QUIC datagram */ + struct quic_dgram { + void *owner; +@@ -38,6 +41,8 @@ struct quic_dgram { + + struct list recv_list; /* elemt to quic_receiver_buf . */ + struct mt_list handler_list; /* elem to quic_dghdlr . */ ++ ++ int flags; /* QUIC_DGRAM_FL_* values */ + }; + + /* QUIC datagram handler */ +diff --git a/src/quic_conn.c b/src/quic_conn.c +index cb56fbe..d9808d2 100644 +--- a/src/quic_conn.c ++++ b/src/quic_conn.c +@@ -56,6 +56,7 @@ + #include + #include + #include ++#include + #include + #include + #include +@@ -478,6 +479,30 @@ int quic_build_post_handshake_frames(struct quic_conn *qc) + } + + LIST_APPEND(&frm_list, &frm->list); ++ ++#ifdef HAVE_SSL_0RTT_QUIC ++ if (qc->li->bind_conf->ssl_conf.early_data) { ++ size_t new_token_frm_len; ++ ++ frm = qc_frm_alloc(QUIC_FT_NEW_TOKEN); ++ if (!frm) { ++ TRACE_ERROR("frame allocation error", QUIC_EV_CONN_IO_CB, qc); ++ goto leave; ++ } ++ ++ new_token_frm_len = ++ quic_generate_token(frm->new_token.data, ++ sizeof(frm->new_token.data), &qc->peer_addr); ++ if (!new_token_frm_len) { ++ TRACE_ERROR("token generation failed", QUIC_EV_CONN_IO_CB, qc); ++ goto leave; ++ } ++ ++ BUG_ON(new_token_frm_len != sizeof(frm->new_token.data)); ++ frm->new_token.len = new_token_frm_len; ++ LIST_APPEND(&frm_list, &frm->list); ++ } ++#endif + } + + /* Initialize connection IDs minus one: there is +@@ -759,6 +784,11 @@ struct task *quic_conn_io_cb(struct task *t, void *context, unsigned int state) + qc_ssl_provide_all_quic_data(qc, qc->xprt_ctx); + } + ++ if (qc->flags & QUIC_FL_CONN_TO_KILL) { ++ TRACE_DEVEL("connection to be killed", QUIC_EV_CONN_PHPKTS, qc); ++ goto out; ++ } ++ + /* Retranmissions */ + if (qc->flags & QUIC_FL_CONN_RETRANS_NEEDED) { + TRACE_DEVEL("retransmission needed", QUIC_EV_CONN_PHPKTS, qc); +@@ -872,7 +902,25 @@ struct task *quic_conn_io_cb(struct task *t, void *context, unsigned int state) + quic_nictx_free(qc); + } + +- if ((qc->flags & QUIC_FL_CONN_CLOSING) && qc->mux_state != QC_MUX_READY) { ++ if (qc->flags & QUIC_FL_CONN_SEND_RETRY) { ++ struct quic_counters *prx_counters; ++ struct proxy *prx = qc->li->bind_conf->frontend; ++ struct quic_rx_packet pkt = { ++ .scid = qc->dcid, ++ .dcid = qc->odcid, ++ }; ++ ++ prx_counters = EXTRA_COUNTERS_GET(prx->extra_counters_fe, &quic_stats_module); ++ if (send_retry(qc->li->rx.fd, &qc->peer_addr, &pkt, qc->original_version)) { ++ TRACE_ERROR("Error during Retry generation", ++ QUIC_EV_CONN_LPKT, NULL, NULL, NULL, qc->original_version); ++ } ++ else ++ HA_ATOMIC_INC(&prx_counters->retry_sent); ++ } ++ ++ if ((qc->flags & (QUIC_FL_CONN_CLOSING|QUIC_FL_CONN_TO_KILL)) && ++ qc->mux_state != QC_MUX_READY) { + quic_conn_release(qc); + qc = NULL; + } +@@ -979,11 +1027,15 @@ struct task *qc_process_timer(struct task *task, void *ctx, unsigned int state) + * for QUIC servers (or haproxy listeners). + * is the destination connection ID, is the source connection ID. + * This latter CID as the same value on the wire as the one for +- * which is the first CID of this connection but a different internal representation used to build ++ * which is the first CID of this connection but a different internal ++ * representation used to build + * NEW_CONNECTION_ID frames. This is the responsibility of the caller to insert + * in the CIDs tree for this connection (qc->cids). +- * is the token found to be used for this connection with as +- * length. Endpoints addresses are specified via and . ++ * is a boolean denoting if a token was received for this connection ++ * from an Initial packet. ++ * is the original destination connection ID which was embedded ++ * into the Retry token sent to the client before instantiated this connection. ++ * Endpoints addresses are specified via and . + * Returns the connection if succeeded, NULL if not. + */ + struct quic_conn *qc_new_conn(const struct quic_version *qv, int ipv4, +@@ -1090,6 +1142,9 @@ struct quic_conn *qc_new_conn(const struct quic_version *qv, int ipv4, + qc->prx_counters = EXTRA_COUNTERS_GET(prx->extra_counters_fe, + &quic_stats_module); + qc->flags = QUIC_FL_CONN_LISTENER; ++ /* Mark this connection as having not received any token when 0-RTT is enabled. */ ++ if (l->bind_conf->ssl_conf.early_data && !token) ++ qc->flags |= QUIC_FL_CONN_NO_TOKEN_RCVD; + qc->state = QUIC_HS_ST_SERVER_INITIAL; + /* Copy the client original DCID. */ + qc->odcid = *dcid; +@@ -1112,7 +1167,7 @@ struct quic_conn *qc_new_conn(const struct quic_version *qv, int ipv4, + /* If connection is instantiated due to an INITIAL packet with an + * already checked token, consider the peer address as validated. + */ +- if (token_odcid->len) { ++ if (token) { + TRACE_STATE("validate peer address due to initial token", + QUIC_EV_CONN_INIT, qc); + qc->flags |= QUIC_FL_CONN_PEER_VALIDATED_ADDR; +diff --git a/src/quic_retry.c b/src/quic_retry.c +index 2d6ea31..78ef88a 100644 +--- a/src/quic_retry.c ++++ b/src/quic_retry.c +@@ -258,17 +258,11 @@ int quic_retry_token_check(struct quic_rx_packet *pkt, + TRACE_ENTER(QUIC_EV_CONN_LPKT, qc); + + /* The caller must ensure this. */ +- BUG_ON(!pkt->token_len); ++ BUG_ON(!pkt->token_len || *pkt->token != QUIC_TOKEN_FMT_RETRY); + + prx = l->bind_conf->frontend; + prx_counters = EXTRA_COUNTERS_GET(prx->extra_counters_fe, &quic_stats_module); + +- if (*pkt->token != QUIC_TOKEN_FMT_RETRY) { +- /* TODO: New token check */ +- TRACE_PROTO("Packet dropped", QUIC_EV_CONN_LPKT, qc, NULL, NULL, pkt->version); +- goto leave; +- } +- + if (sizeof buf < tokenlen) { + TRACE_ERROR("too short buffer", QUIC_EV_CONN_LPKT, qc); + goto err; +diff --git a/src/quic_rx.c b/src/quic_rx.c +index 7bc5844..81eaa69 100644 +--- a/src/quic_rx.c ++++ b/src/quic_rx.c +@@ -26,6 +26,7 @@ + #include + #include + #include ++#include + #include + #include + #include +@@ -1587,6 +1588,47 @@ static inline int quic_padding_check(const unsigned char *pos, + return pos == end; + } + ++/* Validate the token, retry or not (provided by NEW_TOKEN) parsed into ++ * RX packet from datagram. ++ * Return 1 if succeded, 0 if not. ++ */ ++static inline int quic_token_validate(struct quic_rx_packet *pkt, ++ struct quic_dgram *dgram, ++ struct listener *l, struct quic_conn *qc, ++ struct quic_cid *odcid) ++{ ++ int ret = 0; ++ ++ TRACE_ENTER(QUIC_EV_CONN_LPKT, qc); ++ ++ switch (*pkt->token) { ++ case QUIC_TOKEN_FMT_RETRY: ++ ret = quic_retry_token_check(pkt, dgram, l, qc, odcid); ++ break; ++ case QUIC_TOKEN_FMT_NEW: ++ ret = quic_token_check(pkt, dgram, qc); ++ if (!ret) { ++ /* Fallback to a retry token in case of any error. */ ++ dgram->flags |= QUIC_DGRAM_FL_SEND_RETRY; ++ } ++ break; ++ default: ++ TRACE_PROTO("Packet dropped", QUIC_EV_CONN_LPKT, qc, NULL, NULL, pkt->version); ++ break; ++ } ++ ++ if (!ret) ++ goto err; ++ ++ ret = 1; ++ leave: ++ TRACE_LEAVE(QUIC_EV_CONN_LPKT, qc); ++ return ret; ++ err: ++ TRACE_DEVEL("leaving in error", QUIC_EV_CONN_LPKT, qc); ++ goto leave; ++} ++ + /* Find the associated connection to the packet or create a new one if + * this is an Initial packet. is the datagram containing the packet and + * is the listener instance on which it was received. +@@ -1645,22 +1687,38 @@ static struct quic_conn *quic_rx_pkt_retrieve_conn(struct quic_rx_packet *pkt, + } + + if (pkt->token_len) { +- /* Validate the token only when connection is unknown. */ +- if (!quic_retry_token_check(pkt, dgram, l, qc, &token_odcid)) ++ TRACE_PROTO("Initial with token", QUIC_EV_CONN_LPKT, NULL, NULL, NULL, pkt->version); ++ /* Validate the token, retry or not only when connection is unknown. */ ++ if (!quic_token_validate(pkt, dgram, l, qc, &token_odcid)) { ++ if (dgram->flags & QUIC_DGRAM_FL_SEND_RETRY) { ++ if (send_retry(l->rx.fd, &dgram->saddr, pkt, pkt->version)) { ++ TRACE_ERROR("Error during Retry generation", ++ QUIC_EV_CONN_LPKT, NULL, NULL, NULL, pkt->version); ++ } ++ else ++ HA_ATOMIC_INC(&prx_counters->retry_sent); ++ ++ goto out; ++ } ++ + goto err; ++ } + } +- else if (!(l->bind_conf->options & BC_O_QUIC_FORCE_RETRY) && +- HA_ATOMIC_LOAD(&prx_counters->half_open_conn) >= global.tune.quic_retry_threshold) { +- TRACE_PROTO("Initial without token, sending retry", +- QUIC_EV_CONN_LPKT, NULL, NULL, NULL, pkt->version); +- if (send_retry(l->rx.fd, &dgram->saddr, pkt, pkt->version)) { +- TRACE_ERROR("Error during Retry generation", ++ else { ++ TRACE_PROTO("Initial without token", QUIC_EV_CONN_LPKT, NULL, NULL, NULL, pkt->version); ++ if (!(l->bind_conf->options & BC_O_QUIC_FORCE_RETRY) && ++ HA_ATOMIC_LOAD(&prx_counters->half_open_conn) >= global.tune.quic_retry_threshold) { ++ TRACE_PROTO("Initial without token, sending retry", + QUIC_EV_CONN_LPKT, NULL, NULL, NULL, pkt->version); ++ if (send_retry(l->rx.fd, &dgram->saddr, pkt, pkt->version)) { ++ TRACE_ERROR("Error during Retry generation", ++ QUIC_EV_CONN_LPKT, NULL, NULL, NULL, pkt->version); ++ goto out; ++ } ++ ++ HA_ATOMIC_INC(&prx_counters->retry_sent); + goto out; + } +- +- HA_ATOMIC_INC(&prx_counters->retry_sent); +- goto out; + } + + /* RFC 9000 7.2. Negotiating Connection IDs: +diff --git a/src/quic_sock.c b/src/quic_sock.c +index 7a18bac..6713bdb 100644 +--- a/src/quic_sock.c ++++ b/src/quic_sock.c +@@ -292,6 +292,7 @@ static int quic_lstnr_dgram_dispatch(unsigned char *pos, size_t len, void *owner + dgram->saddr = *saddr; + dgram->daddr = *daddr; + dgram->qc = NULL; ++ dgram->flags = 0; + + /* Attached datagram to its quic_receiver_buf and quic_dghdlrs. */ + LIST_APPEND(dgrams, &dgram->recv_list); +@@ -778,6 +779,7 @@ int qc_rcv_buf(struct quic_conn *qc) + new_dgram->saddr = saddr; + new_dgram->daddr = daddr; + new_dgram->qc = NULL; /* set later via quic_dgram_parse() */ ++ new_dgram->flags = 0; + + TRACE_DEVEL("read datagram", QUIC_EV_CONN_RCV, qc, new_dgram); + +diff --git a/src/quic_ssl.c b/src/quic_ssl.c +index 73bf8dc..b48494e 100644 +--- a/src/quic_ssl.c ++++ b/src/quic_ssl.c +@@ -354,6 +354,23 @@ static int ha_quic_add_handshake_data(SSL *ssl, enum ssl_encryption_level_t leve + + TRACE_ENTER(QUIC_EV_CONN_ADDDATA, qc); + ++ TRACE_PROTO("ha_quic_add_handshake_data() called", QUIC_EV_CONN_IO_CB, qc, NULL, ssl); ++ ++#ifdef HAVE_SSL_0RTT_QUIC ++ /* Detect asap if some 0-RTT data were accepted for this connection. ++ * If this is the case and no token was provided, interrupt the useless ++ * secrets derivations. A Retry packet must be sent, and this connection ++ * must be killed. ++ * Note that QUIC_FL_CONN_NO_TOKEN_RCVD is possibly set only for when 0-RTT is ++ * enabled for the connection. ++ */ ++ if ((qc->flags & QUIC_FL_CONN_NO_TOKEN_RCVD) && qc_ssl_eary_data_accepted(ssl)) { ++ TRACE_PROTO("connection to be killed", QUIC_EV_CONN_ADDDATA, qc); ++ qc->flags |= QUIC_FL_CONN_TO_KILL|QUIC_FL_CONN_SEND_RETRY; ++ goto leave; ++ } ++#endif ++ + if (qc->flags & QUIC_FL_CONN_TO_KILL) { + TRACE_PROTO("connection to be killed", QUIC_EV_CONN_ADDDATA, qc); + goto out; +@@ -528,9 +545,10 @@ int qc_ssl_provide_quic_data(struct ncbuf *ncbuf, + state = qc->state; + if (state < QUIC_HS_ST_COMPLETE) { + ssl_err = SSL_do_handshake(ctx->ssl); ++ TRACE_PROTO("SSL_do_handshake() called", QUIC_EV_CONN_IO_CB, qc, NULL, ctx->ssl); + + if (qc->flags & QUIC_FL_CONN_TO_KILL) { +- TRACE_DEVEL("connection to be killed", QUIC_EV_CONN_IO_CB, qc); ++ TRACE_DEVEL("connection to be killed", QUIC_EV_CONN_IO_CB, qc, &state, ctx->ssl); + goto leave; + } + +-- +1.7.10.4 + diff --git a/haproxy.spec b/haproxy.spec index 08aa47a..e57a89f 100644 --- a/haproxy.spec +++ b/haproxy.spec @@ -5,7 +5,7 @@ Name: haproxy Version: 2.9.9 -Release: 2 +Release: 3 Summary: The Reliable, High Performance TCP/HTTP Load Balancer License: GPLv2+ @@ -17,6 +17,7 @@ Source3: %{name}.logrotate Source4: %{name}.sysconfig Patch1: Backport-CVE-2024-45506-BUG-MAJOR-mux-h2-always.patch +Patch2: CVE-2024-49214.patch BuildRequires: gcc lua-devel pcre2-devel openssl-devel systemd-devel systemd libatomic Requires(pre): shadow-utils @@ -121,6 +122,9 @@ exit 0 %{_mandir}/man1/* %changelog +* Mon Oct 14 2024 yaoxin - 2.9.9-3 +- Fix CVE-2024-49214 + * Wed Sep 04 2024 yinyongkang - 2.9.9-2 - Type:CVE - CVE:CVE-2024-45506 -- Gitee