CVE-2020-11080 (published 2020-06-03T23:15Z)

In nghttp2 before version 1.41.0, the overly large HTTP/2 SETTINGS frame payload causes denial of service. The proof of concept attack involves a malicious client constructing a SETTINGS frame with a length of 14,400 bytes (2400 individual settings entries) over and over again. The attack causes the CPU to spike at 100%. nghttp2 v1.41.0 fixes this vulnerability. There is a workaround to this vulnerability. Implement nghttp2_on_frame_recv_callback callback, and if received frame is SETTINGS frame and the number of settings entries are large (e.g., > 32), then drop the connection.

Severity: MEDIUM
CVSS v2 base score: 5.0 (AV:N/AC:L/Au:N/C:N/I:N/A:P)
CVSS v3 base score: 7.5, HIGH (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H)

Weakness information:
cwe_id: CWE-707
cwe_name: Improper Neutralization
description: The product does not ensure or incorrectly ensures that structured messages or data are well-formed and that certain security properties are met before being read from an upstream component or sent to a downstream component.
extended_description: 
            If a message is malformed, it may cause the message to be incorrectly interpreted.
            Neutralization is an abstract term for any technique that ensures that input (and output) conforms with expectations and is "safe."  This can be done by:
	    
	      checking that the input/output is already "safe" (e.g. validation)
	      transformation of the input/output to be "safe" using techniques such as filtering, encoding/decoding, escaping/unescaping, quoting/unquoting, or canonicalization
	      preventing the input/output from being directly provided by an attacker (e.g. "indirect selection" that maps externally-provided values to internally-controlled values)
	      preventing the input/output from being processed at all
	    
            This weakness typically applies in cases where the product prepares a control message that another process must act on, such as a command or query, and malicious input that was intended as data, can enter the control plane instead. However, this weakness also applies to more general cases where there are not always control implications.
         
         

Fix information:
repo_url: https://github.com/nghttp2/nghttp2
commit_details: repo_url: https://github.com/nghttp2/nghttp2
author: James M Snell
author_date: 2020-04-19 09:12:24-07:00
msg: Earlier check for settings flood
repository: repo_url: https://github.com/nghttp2/nghttp2
repo_name: nghttp2/nghttp2
description: nghttp2 - HTTP/2 C Library and tools
repo_language: C++
owner: nghttp2
file_changes: filename: nghttp2_session.c
change_type: ModificationType.MODIFY
diff: @@ -5653,6 +5653,12 @@ ssize_t nghttp2_session_mem_recv(nghttp2_session *session, const uint8_t *in,
           break;
         }
 
+        /* Check the settings flood counter early to be safe */
+        if (session->obq_flood_counter_ >= session->max_outbound_ack &&
+            !(iframe->frame.hd.flags & NGHTTP2_FLAG_ACK)) {
+          return NGHTTP2_ERR_FLOODED;
+        }
+
         iframe->state = NGHTTP2_IB_READ_SETTINGS;
 
         if (iframe->payloadleft) {

programming_language: C
repo_url: https://github.com/nghttp2/nghttp2
commit_details: repo_url: https://github.com/nghttp2/nghttp2
author: James M Snell
author_date: 2020-04-17 16:53:51-07:00
msg: Implement max settings option
repository: repo_url: https://github.com/nghttp2/nghttp2
repo_name: nghttp2/nghttp2
description: nghttp2 - HTTP/2 C Library and tools
repo_language: C++
owner: nghttp2
file_changes: filename: CMakeLists.txt
change_type: ModificationType.MODIFY
diff: @@ -42,6 +42,7 @@ set(APIDOCS
   nghttp2_option_set_no_recv_client_magic.rst
   nghttp2_option_set_peer_max_concurrent_streams.rst
   nghttp2_option_set_user_recv_extension_type.rst
+  nghttp2_option_set_max_settings.rst
   nghttp2_pack_settings_payload.rst
   nghttp2_priority_spec_check_default.rst
   nghttp2_priority_spec_default_init.rst

programming_language: Shell
filename: Makefile.am
change_type: ModificationType.MODIFY
diff: @@ -69,6 +69,7 @@ APIDOCS= \
 	nghttp2_option_set_peer_max_concurrent_streams.rst \
 	nghttp2_option_set_user_recv_extension_type.rst \
 	nghttp2_option_set_max_outbound_ack.rst \
+	nghttp2_option_set_max_settings.rst \
 	nghttp2_pack_settings_payload.rst \
 	nghttp2_priority_spec_check_default.rst \
 	nghttp2_priority_spec_default_init.rst \

programming_language: Shell
filename: nghttp2.h
change_type: ModificationType.MODIFY
diff: @@ -228,6 +228,13 @@ typedef struct {
  */
 #define NGHTTP2_CLIENT_MAGIC_LEN 24
 
+/**
+ * @macro
+ *
+ * The default max number of settings per SETTINGS frame
+ */
+#define NGHTTP2_DEFAULT_MAX_SETTINGS 32
+
 /**
  * @enum
  *
@@ -398,6 +405,11 @@ typedef enum {
    * receives an other type of frame.
    */
   NGHTTP2_ERR_SETTINGS_EXPECTED = -536,
+  /**
+   * When a local endpoint receives too many settings entries
+   * in a single SETTINGS frame.
+   */
+  NGHTTP2_ERR_TOO_MANY_SETTINGS = -537,
   /**
    * The errors < :enum:`NGHTTP2_ERR_FATAL` mean that the library is
    * under unexpected condition and processing was terminated (e.g.,
@@ -2659,6 +2671,17 @@ NGHTTP2_EXTERN void nghttp2_option_set_no_closed_streams(nghttp2_option *option,
 NGHTTP2_EXTERN void nghttp2_option_set_max_outbound_ack(nghttp2_option *option,
                                                         size_t val);
 
+/**
+ * @function
+ *
+ * This function sets the maximum number of SETTINGS entries per
+ * SETTINGS frame that will be accepted. If more than those entries
+ * are received, the peer is considered to be misbehaving and session
+ * will be closed. The default value is 32.
+ */
+NGHTTP2_EXTERN void nghttp2_option_set_max_settings(nghttp2_option *option,
+                                                    size_t val);
+
 /**
  * @function
  *

programming_language: C
filename: nghttp2_helper.c
change_type: ModificationType.MODIFY
diff: @@ -334,6 +334,8 @@ const char *nghttp2_strerror(int error_code) {
   case NGHTTP2_ERR_FLOODED:
     return "Flooding was detected in this HTTP/2 session, and it must be "
            "closed";
+  case NGHTTP2_ERR_TOO_MANY_SETTINGS:
+    return "SETTINGS frame contained more than the maximum allowed entries";
   default:
     return "Unknown error code";
   }

programming_language: C
filename: nghttp2_option.c
change_type: ModificationType.MODIFY
diff: @@ -121,3 +121,8 @@ void nghttp2_option_set_max_outbound_ack(nghttp2_option *option, size_t val) {
   option->opt_set_mask |= NGHTTP2_OPT_MAX_OUTBOUND_ACK;
   option->max_outbound_ack = val;
 }
+
+void nghttp2_option_set_max_settings(nghttp2_option *option, size_t val) {
+  option->opt_set_mask |= NGHTTP2_OPT_MAX_SETTINGS;
+  option->max_settings = val;
+}

programming_language: C
filename: nghttp2_option.h
change_type: ModificationType.MODIFY
diff: @@ -67,6 +67,7 @@ typedef enum {
   NGHTTP2_OPT_MAX_DEFLATE_DYNAMIC_TABLE_SIZE = 1 << 9,
   NGHTTP2_OPT_NO_CLOSED_STREAMS = 1 << 10,
   NGHTTP2_OPT_MAX_OUTBOUND_ACK = 1 << 11,
+  NGHTTP2_OPT_MAX_SETTINGS = 1 << 12,
 } nghttp2_option_flag;
 
 /**
@@ -85,6 +86,10 @@ struct nghttp2_option {
    * NGHTTP2_OPT_MAX_OUTBOUND_ACK
    */
   size_t max_outbound_ack;
+  /**
+   * NGHTTP2_OPT_MAX_SETTINGS
+   */
+  size_t max_settings;
   /**
    * Bitwise OR of nghttp2_option_flag to determine that which fields
    * are specified.

programming_language: C
filename: nghttp2_session.c
change_type: ModificationType.MODIFY
diff: @@ -458,6 +458,7 @@ static int session_new(nghttp2_session **session_ptr,
 
   (*session_ptr)->max_send_header_block_length = NGHTTP2_MAX_HEADERSLEN;
   (*session_ptr)->max_outbound_ack = NGHTTP2_DEFAULT_MAX_OBQ_FLOOD_ITEM;
+  (*session_ptr)->max_settings = NGHTTP2_DEFAULT_MAX_SETTINGS;
 
   if (option) {
     if ((option->opt_set_mask & NGHTTP2_OPT_NO_AUTO_WINDOW_UPDATE) &&
@@ -521,6 +522,11 @@ static int session_new(nghttp2_session **session_ptr,
     if (option->opt_set_mask & NGHTTP2_OPT_MAX_OUTBOUND_ACK) {
       (*session_ptr)->max_outbound_ack = option->max_outbound_ack;
     }
+
+    if ((option->opt_set_mask & NGHTTP2_OPT_MAX_SETTINGS) &&
+        option->max_settings) {
+      (*session_ptr)->max_settings = option->max_settings;
+    }
   }
 
   rv = nghttp2_hd_deflate_init2(&(*session_ptr)->hd_deflater,
@@ -5657,6 +5663,16 @@ ssize_t nghttp2_session_mem_recv(nghttp2_session *session, const uint8_t *in,
           iframe->max_niv =
               iframe->frame.hd.length / NGHTTP2_FRAME_SETTINGS_ENTRY_LENGTH + 1;
 
+          if (iframe->max_niv - 1 > session->max_settings) {
+            rv = nghttp2_session_terminate_session_with_reason(
+                session, NGHTTP2_ENHANCE_YOUR_CALM,
+                "SETTINGS: too many setting entries");
+            if (nghttp2_is_fatal(rv)) {
+              return rv;
+            }
+            return (ssize_t)inlen;
+          }
+
           iframe->iv = nghttp2_mem_malloc(mem, sizeof(nghttp2_settings_entry) *
                                                    iframe->max_niv);
 
@@ -7425,6 +7441,11 @@ static int nghttp2_session_upgrade_internal(nghttp2_session *session,
   if (settings_payloadlen % NGHTTP2_FRAME_SETTINGS_ENTRY_LENGTH) {
     return NGHTTP2_ERR_INVALID_ARGUMENT;
   }
+  /* SETTINGS frame contains too many settings */
+  if (settings_payloadlen / NGHTTP2_FRAME_SETTINGS_ENTRY_LENGTH
+        > session->max_settings) {
+    return NGHTTP2_ERR_TOO_MANY_SETTINGS;
+  }
   rv = nghttp2_frame_unpack_settings_payload2(&iv, &niv, settings_payload,
                                               settings_payloadlen, mem);
   if (rv != 0) {

programming_language: C
filename: nghttp2_session.h
change_type: ModificationType.MODIFY
diff: @@ -267,6 +267,8 @@ struct nghttp2_session {
   /* The maximum length of header block to send.  Calculated by the
      same way as nghttp2_hd_deflate_bound() does. */
   size_t max_send_header_block_length;
+  /* The maximum number of settings accepted per SETTINGS frame. */
+  size_t max_settings;
   /* Next Stream ID. Made unsigned int to detect >= (1 << 31). */
   uint32_t next_stream_id;
   /* The last stream ID this session initiated.  For client session,

programming_language: C
filename: main.c
change_type: ModificationType.MODIFY
diff: @@ -317,6 +317,8 @@ int main() {
                    test_nghttp2_session_set_local_window_size) ||
       !CU_add_test(pSuite, "session_cancel_from_before_frame_send",
                    test_nghttp2_session_cancel_from_before_frame_send) ||
+      !CU_add_test(pSuite, "session_too_many_settings",
+                   test_nghttp2_session_too_many_settings) ||
       !CU_add_test(pSuite, "session_removed_closed_stream",
                    test_nghttp2_session_removed_closed_stream) ||
       !CU_add_test(pSuite, "session_pause_data",

programming_language: SQL
filename: nghttp2_session_test.c
change_type: ModificationType.MODIFY
diff: @@ -10614,6 +10614,67 @@ void test_nghttp2_session_cancel_from_before_frame_send(void) {
   nghttp2_session_del(session);
 }
 
+void test_nghttp2_session_too_many_settings(void) {
+  nghttp2_session *session;
+  nghttp2_option *option;
+  nghttp2_session_callbacks callbacks;
+  nghttp2_frame frame;
+  nghttp2_bufs bufs;
+  nghttp2_buf *buf;
+  ssize_t rv;
+  my_user_data ud;
+  nghttp2_settings_entry iv[3];
+  nghttp2_mem *mem;
+  nghttp2_outbound_item *item;
+
+  mem = nghttp2_mem_default();
+  frame_pack_bufs_init(&bufs);
+
+  memset(&callbacks, 0, sizeof(nghttp2_session_callbacks));
+  callbacks.on_frame_recv_callback = on_frame_recv_callback;
+  callbacks.send_callback = null_send_callback;
+
+  nghttp2_option_new(&option);
+  nghttp2_option_set_max_settings(option, 1);
+
+  nghttp2_session_client_new2(&session, &callbacks, &ud, option);
+
+  CU_ASSERT(1 == session->max_settings);
+
+  nghttp2_option_del(option);
+
+  iv[0].settings_id = NGHTTP2_SETTINGS_HEADER_TABLE_SIZE;
+  iv[0].value = 3000;
+
+  iv[1].settings_id = NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE;
+  iv[1].value = 16384;
+
+  nghttp2_frame_settings_init(&frame.settings, NGHTTP2_FLAG_NONE, dup_iv(iv, 2),
+                              2);
+
+  rv = nghttp2_frame_pack_settings(&bufs, &frame.settings);
+
+  CU_ASSERT(0 == rv);
+  CU_ASSERT(nghttp2_bufs_len(&bufs) > 0);
+
+  nghttp2_frame_settings_free(&frame.settings, mem);
+
+  buf = &bufs.head->buf;
+  assert(nghttp2_bufs_len(&bufs) == nghttp2_buf_len(buf));
+
+  ud.frame_recv_cb_called = 0;
+
+  rv = nghttp2_session_mem_recv(session, buf->pos, nghttp2_buf_len(buf));
+  CU_ASSERT((ssize_t)nghttp2_buf_len(buf) == rv);
+
+  item = nghttp2_session_get_next_ob_item(session);
+  CU_ASSERT(NGHTTP2_GOAWAY == item->frame.hd.type);
+
+  nghttp2_bufs_reset(&bufs);
+  nghttp2_bufs_free(&bufs);
+  nghttp2_session_del(session);
+}
+
 static void
 prepare_session_removed_closed_stream(nghttp2_session *session,
                                       nghttp2_hd_deflater *deflater) {

programming_language: C
filename: nghttp2_session_test.h
change_type: ModificationType.MODIFY
diff: @@ -156,6 +156,7 @@ void test_nghttp2_session_repeated_priority_change(void);
 void test_nghttp2_session_repeated_priority_submission(void);
 void test_nghttp2_session_set_local_window_size(void);
 void test_nghttp2_session_cancel_from_before_frame_send(void);
+void test_nghttp2_session_too_many_settings(void);
 void test_nghttp2_session_removed_closed_stream(void);
 void test_nghttp2_session_pause_data(void);
 void test_nghttp2_session_no_closed_streams(void);

programming_language: C