From 15b42e9faa555d8f9008d50b5f7605799ee91c6e Mon Sep 17 00:00:00 2001 From: Jon Shallow Date: Tue, 11 Dec 2018 14:20:25 +0000 Subject: [PATCH] Possible to crash coap-server with uncompleted (D)TLS handshakes For any TLS implementation, it is possible to crash the coap-server by initiating (D)TLS handshakes but never completing them, thus continuing to use more RAM. Each partially completed handshake is left in the endpoint->sessions chain in a state of COAP_SESSION_STATE_HANDSHAKE and unless endpoint->max_idle_sessions is deliberately configured, these sessions will never get deleted. A cause of partially completed handshakes is when there is a PSK mismatch. Furthermore, unnecessary CPU usage is used to search down long lists held in endpoint->sessions. This fix limits the number of these sessions in a handshake state to the number defined by endpoint->max_handshake_sessions if non zero or 100 to prevent memory exhaustion. By default, endpoint->max_handshake_sessions is 0. Sessions in state COAP_SESSION_STATE_HANDSHAKE will get timed out after 30 seconds of inactivity. This does not prevent a DOS attack by creating max_handshake_sessions as a genuine new session will fail to get responded to until the number of current COAP_SESSION_STATE_HANDSHAKE sessions drop. However, the use of Cookie exchanges by the underlying TLS library prior to a new session being created and added into endpoint->sessions will filter out a lot of the DOS attacks. include/coap2/coap_session.h New default definition for COAP_DEFAULT_MAX_HANDSHAKE_SESSIONS New default definition for COAP_PARTIAL_SESSION_TIMEOUT_TICKS include/coap2/net.h Include max_handshake_sessions in coap_context_t src/coap_session.c In coap_endpoint_get_session() check the number of sessions in COAP_SESSION_STATE_HANDSHAKE and handle accordingly when limits are hit, or they need to be timed out. --- include/coap2/coap_session.h | 2 ++ include/coap2/net.h | 1 + src/coap_session.c | 35 +++++++++++++++++++++++++++++++++-- 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/include/coap2/coap_session.h b/include/coap2/coap_session.h index a02e1916..4bfa811a 100644 --- a/include/coap2/coap_session.h +++ b/include/coap2/coap_session.h @@ -29,6 +29,8 @@ typedef struct coap_fixed_point_t { } coap_fixed_point_t; #define COAP_DEFAULT_SESSION_TIMEOUT 300 +#define COAP_PARTIAL_SESSION_TIMEOUT_TICKS (30 * COAP_TICKS_PER_SECOND) +#define COAP_DEFAULT_MAX_HANDSHAKE_SESSIONS 100 #define COAP_PROTO_NOT_RELIABLE(p) ((p)==COAP_PROTO_UDP || (p)==COAP_PROTO_DTLS) #define COAP_PROTO_RELIABLE(p) ((p)==COAP_PROTO_TCP || (p)==COAP_PROTO_TLS) diff --git a/include/coap2/net.h b/include/coap2/net.h index 1b89c0f1..b7ee023c 100644 --- a/include/coap2/net.h +++ b/include/coap2/net.h @@ -212,6 +212,7 @@ typedef struct coap_context_t { unsigned int session_timeout; /**< Number of seconds of inactivity after which an unused session will be closed. 0 means use default. */ unsigned int max_idle_sessions; /**< Maximum number of simultaneous unused sessions per endpoint. 0 means no maximum. */ + unsigned int max_handshake_sessions; /**< Maximum number of simultaneous negotating sessions per endpoint. 0 means use default. */ unsigned int ping_timeout; /**< Minimum inactivity time before sending a ping message. 0 means disabled. */ unsigned int csm_timeout; /**< Timeout for waiting for a CSM from the remote side. 0 means disabled. */ diff --git a/src/coap_session.c b/src/coap_session.c index 89244196..95005197 100644 --- a/src/coap_session.c +++ b/src/coap_session.c @@ -445,7 +445,9 @@ coap_endpoint_get_session(coap_endpoint_t *endpoint, const coap_packet_t *packet, coap_tick_t now) { coap_session_t *session = NULL; unsigned int num_idle = 0; + unsigned int num_hs = 0; coap_session_t *oldest = NULL; + coap_session_t *oldest_hs = NULL; endpoint->hello.ifindex = -1; @@ -457,15 +459,44 @@ coap_endpoint_get_session(coap_endpoint_t *endpoint, session->last_rx_tx = now; return session; } - if (session->ref == 0 && session->delayqueue == NULL && session->type == COAP_SESSION_TYPE_SERVER) { + if (session->ref == 0 && session->delayqueue == NULL && + session->type == COAP_SESSION_TYPE_SERVER) { ++num_idle; if (oldest==NULL || session->last_rx_tx < oldest->last_rx_tx) oldest = session; + + if (session->state == COAP_SESSION_STATE_HANDSHAKE) { + ++num_hs; + /* See if this is a partial SSL session set up + which needs to be cleared down to prevent DOS */ + if ((session->last_rx_tx + COAP_PARTIAL_SESSION_TIMEOUT_TICKS) < now) { + if (oldest_hs == NULL || + session->last_rx_tx < oldest_hs->last_rx_tx) + oldest_hs = session; + } + } } } - if (endpoint->context->max_idle_sessions > 0 && num_idle >= endpoint->context->max_idle_sessions) + if (endpoint->context->max_idle_sessions > 0 && + num_idle >= endpoint->context->max_idle_sessions) { coap_session_free(oldest); + } + else if (oldest_hs) { + coap_log(LOG_WARNING, "***%s: Incomplete session timed out\n", + coap_session_str(oldest_hs)); + coap_session_free(oldest_hs); + } + + if (num_hs > (endpoint->context->max_handshake_sessions ? + endpoint->context->max_handshake_sessions : + COAP_DEFAULT_MAX_HANDSHAKE_SESSIONS)) { + /* Maxed out on number of session in SSL negotiation state */ + coap_log(LOG_DEBUG, + "Oustanding sessions in COAP_SESSION_STATE_HANDSHAKE too " + "large. New request ignored\n"); + return NULL; + } if (endpoint->proto == COAP_PROTO_DTLS) { session = &endpoint->hello;