Compare commits

...

10 Commits

Author SHA1 Message Date
openeuler-ci-bot
a3a6fe9720
!188 correct error related to CVE-2025-23085
From: @aaronhans 
Reviewed-by: @wang--ge 
Signed-off-by: @wang--ge
2025-04-11 06:42:24 +00:00
hanguanqiang
5f9f617f08 1.According to the description above error line in node_http2.cc,this should be checking whether frame->hd.type is NGHTTP2_GOAWAY,and the value of NGHTTP2_GOAWAY is 0x07,however,it is written as 0x03 here,which i think it is an error;2.correct the error of http2 header frame content based on http2 related protocal so that make UT script exited successfully 2025-04-11 09:40:05 +08:00
openeuler-ci-bot
7995eae96c
!180 [sync] PR-179: Fix CVE-2025-23085
From: @openeuler-sync-bot 
Reviewed-by: @wang--ge 
Signed-off-by: @wang--ge
2025-03-06 03:30:16 +00:00
starlet-dx
842b99160b Fix CVE-2025-23085
(cherry picked from commit 0ffb6bb72a390b4880722f9d3005e40a20bacb1a)
2025-03-06 10:10:47 +08:00
openeuler-ci-bot
927c97fd08
!165 [sync] PR-162: Fix CVE-2023-46809,CVE-2024-22019,CVE-2024-22025,CVE-2024-27982 and CVE-2024-27983
From: @openeuler-sync-bot 
Reviewed-by: @wang--ge 
Signed-off-by: @wang--ge
2024-09-19 09:49:03 +00:00
starlet-dx
e0ba077bad Fix CVE-2023-46809,CVE-2024-22019,CVE-2024-22025,CVE-2024-27982 and CVE-2024-27983
(cherry picked from commit e7f6c2348b691a703920fea16a74541b2a4f3fb0)
2024-09-19 16:23:44 +08:00
openeuler-ci-bot
a10a1f78cb
!145 [sync] PR-141: Fix CVE-2023-44487
From: @openeuler-sync-bot 
Reviewed-by: @wang--ge 
Signed-off-by: @wang--ge
2024-02-07 01:20:30 +00:00
starlet-dx
c20d39d2dc Fix CVE-2023-44487
(cherry picked from commit fc686884e1f132e8f626285023940d7731638394)
2024-02-06 18:08:01 +08:00
openeuler-ci-bot
8292fa5976
!139 [sync] PR-136: Fix CVE-2023-0464 and CVE-2023-0465
From: @openeuler-sync-bot 
Reviewed-by: @wang--ge 
Signed-off-by: @wang--ge
2024-02-05 12:22:41 +00:00
starlet-dx
ced2afd58b Fix CVE-2023-0464 and CVE-2023-0465
(cherry picked from commit 79987a7bfac0e6cb2fe1ea19ff6644ad1f71324e)
2024-02-05 18:00:52 +08:00
12 changed files with 51685 additions and 1 deletions

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,51 @@
From 99350cc54fbd14e9294fed5b5b0ef7eb99c25d8b Mon Sep 17 00:00:00 2001
From: hanguanqiang <hanguanqiang@kylinos.cn>
Date: Fri, 11 Apr 2025 07:55:50 +0800
Subject: [PATCH] correct-some-errors-related-to-CVE-2025-23085
---
src/node_http2.cc | 2 +-
test/parallel/test-http2-premature-close.js | 6 +++---
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/node_http2.cc b/src/node_http2.cc
index 6365734..ac59ce9 100644
--- a/src/node_http2.cc
+++ b/src/node_http2.cc
@@ -1048,7 +1048,7 @@ int Http2Session::OnFrameNotSent(nghttp2_session* handle,
// closed but the Http2Session will still be up causing a memory leak.
// Therefore, if the GOAWAY frame couldn't be send due to
// ERR_SESSION_CLOSING we should force close from our side.
- if (frame->hd.type != 0x03) {
+ if (frame->hd.type != NGHTTP2_GOAWAY) {
return 0;
}
}
diff --git a/test/parallel/test-http2-premature-close.js b/test/parallel/test-http2-premature-close.js
index a9b08f5..df30c42 100644
--- a/test/parallel/test-http2-premature-close.js
+++ b/test/parallel/test-http2-premature-close.js
@@ -29,9 +29,9 @@ async function requestAndClose(server) {
// Send a valid HEADERS frame
const headersFrame = Buffer.concat([
Buffer.from([
- 0x00, 0x00, 0x0c, // Length: 12 bytes
+ 0x00, 0x00, 0x0e, // Length: 14 bytes
0x01, // Type: HEADERS
- 0x05, // Flags: END_HEADERS + END_STREAM
+ 0x04, // Flags: END_HEADERS
(streamId >> 24) & 0xFF, // Stream ID: high byte
(streamId >> 16) & 0xFF,
(streamId >> 8) & 0xFF,
@@ -41,7 +41,7 @@ async function requestAndClose(server) {
0x82, // Indexed Header Field Representation (Predefined ":method: GET")
0x84, // Indexed Header Field Representation (Predefined ":path: /")
0x86, // Indexed Header Field Representation (Predefined ":scheme: http")
- 0x44, 0x0a, // Custom ":authority: localhost"
+ 0x41, 0x09, // ":authority: localhost" Length: 9 bytes
0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x68, 0x6f, 0x73, 0x74,
]),
]);
--
2.43.0

219
CVE-2023-0464.patch Normal file
View File

@ -0,0 +1,219 @@
From 879f7080d7e141f415c79eaa3a8ac4a3dad0348b Mon Sep 17 00:00:00 2001
From: Pauli <pauli@openssl.org>
Date: Wed, 8 Mar 2023 15:28:20 +1100
Subject: [PATCH] x509: excessive resource use verifying policy constraints
A security vulnerability has been identified in all supported versions
of OpenSSL related to the verification of X.509 certificate chains
that include policy constraints. Attackers may be able to exploit this
vulnerability by creating a malicious certificate chain that triggers
exponential use of computational resources, leading to a denial-of-service
(DoS) attack on affected systems.
Fixes CVE-2023-0464
Reviewed-by: Tomas Mraz <tomas@openssl.org>
Reviewed-by: Shane Lontis <shane.lontis@oracle.com>
(Merged from https://github.com/openssl/openssl/pull/20569)
---
deps/openssl/openssl/crypto/x509v3/pcy_local.h | 8 +++++++-
deps/openssl/openssl/crypto/x509v3/pcy_node.c | 12 +++++++++---
deps/openssl/openssl/crypto/x509v3/pcy_tree.c | 37 +++++++++++++++++++++++++++----------
3 files changed, 43 insertions(+), 14 deletions(-)
diff --git a/deps/openssl/openssl/crypto/x509v3/pcy_local.h b/deps/openssl/openssl/crypto/x509v3/pcy_local.h
index 5daf78de45850..344aa067659cd 100644
--- a/deps/openssl/openssl/crypto/x509v3/pcy_local.h
+++ b/deps/openssl/openssl/crypto/x509v3/pcy_local.h
@@ -111,6 +111,11 @@ struct X509_POLICY_LEVEL_st {
};
struct X509_POLICY_TREE_st {
+ /* The number of nodes in the tree */
+ size_t node_count;
+ /* The maximum number of nodes in the tree */
+ size_t node_maximum;
+
/* This is the tree 'level' data */
X509_POLICY_LEVEL *levels;
int nlevel;
@@ -159,7 +164,8 @@ X509_POLICY_NODE *tree_find_sk(STACK_OF(X509_POLICY_NODE) *sk,
X509_POLICY_NODE *level_add_node(X509_POLICY_LEVEL *level,
X509_POLICY_DATA *data,
X509_POLICY_NODE *parent,
- X509_POLICY_TREE *tree);
+ X509_POLICY_TREE *tree,
+ int extra_data);
void policy_node_free(X509_POLICY_NODE *node);
int policy_node_match(const X509_POLICY_LEVEL *lvl,
const X509_POLICY_NODE *node, const ASN1_OBJECT *oid);
diff --git a/deps/openssl/openssl/crypto/x509v3/pcy_node.c b/deps/openssl/openssl/crypto/x509v3/pcy_node.c
index e2d7b15322363..d574fb9d665dc 100644
--- a/deps/openssl/openssl/crypto/x509v3/pcy_node.c
+++ b/deps/openssl/openssl/crypto/x509v3/pcy_node.c
@@ -59,10 +59,15 @@ X509_POLICY_NODE *level_find_node(const X509_POLICY_LEVEL *level,
X509_POLICY_NODE *level_add_node(X509_POLICY_LEVEL *level,
X509_POLICY_DATA *data,
X509_POLICY_NODE *parent,
- X509_POLICY_TREE *tree)
+ X509_POLICY_TREE *tree,
+ int extra_data)
{
X509_POLICY_NODE *node;
+ /* Verify that the tree isn't too large. This mitigates CVE-2023-0464 */
+ if (tree->node_maximum > 0 && tree->node_count >= tree->node_maximum)
+ return NULL;
+
node = OPENSSL_zalloc(sizeof(*node));
if (node == NULL) {
X509V3err(X509V3_F_LEVEL_ADD_NODE, ERR_R_MALLOC_FAILURE);
@@ -70,7 +75,7 @@ X509_POLICY_NODE *level_add_node(X509_POLICY_LEVEL *level,
}
node->data = data;
node->parent = parent;
- if (level) {
+ if (level != NULL) {
if (OBJ_obj2nid(data->valid_policy) == NID_any_policy) {
if (level->anyPolicy)
goto node_error;
@@ -90,7 +95,7 @@ X509_POLICY_NODE *level_add_node(X509_POLICY_LEVEL *level,
}
}
- if (tree) {
+ if (extra_data) {
if (tree->extra_data == NULL)
tree->extra_data = sk_X509_POLICY_DATA_new_null();
if (tree->extra_data == NULL){
@@ -103,6 +108,7 @@ X509_POLICY_NODE *level_add_node(X509_POLICY_LEVEL *level,
}
}
+ tree->node_count++;
if (parent)
parent->nchild++;
diff --git a/deps/openssl/openssl/crypto/x509v3/pcy_tree.c b/deps/openssl/openssl/crypto/x509v3/pcy_tree.c
index 6e8322cbc5e38..6c7fd35405000 100644
--- a/deps/openssl/openssl/crypto/x509v3/pcy_tree.c
+++ b/deps/openssl/openssl/crypto/x509v3/pcy_tree.c
@@ -13,6 +13,18 @@
#include "pcy_local.h"
+/*
+ * If the maximum number of nodes in the policy tree isn't defined, set it to
+ * a generous default of 1000 nodes.
+ *
+ * Defining this to be zero means unlimited policy tree growth which opens the
+ * door on CVE-2023-0464.
+ */
+
+#ifndef OPENSSL_POLICY_TREE_NODES_MAX
+# define OPENSSL_POLICY_TREE_NODES_MAX 1000
+#endif
+
/*
* Enable this to print out the complete policy tree at various point during
* evaluation.
@@ -168,6 +180,9 @@ static int tree_init(X509_POLICY_TREE **ptree, STACK_OF(X509) *certs,
return X509_PCY_TREE_INTERNAL;
}
+ /* Limit the growth of the tree to mitigate CVE-2023-0464 */
+ tree->node_maximum = OPENSSL_POLICY_TREE_NODES_MAX;
+
/*
* http://tools.ietf.org/html/rfc5280#section-6.1.2, figure 3.
*
@@ -184,7 +199,7 @@ static int tree_init(X509_POLICY_TREE **ptree, STACK_OF(X509) *certs,
level = tree->levels;
if ((data = policy_data_new(NULL, OBJ_nid2obj(NID_any_policy), 0)) == NULL)
goto bad_tree;
- if (level_add_node(level, data, NULL, tree) == NULL) {
+ if (level_add_node(level, data, NULL, tree, 1) == NULL) {
policy_data_free(data);
goto bad_tree;
}
@@ -243,7 +258,8 @@ static int tree_init(X509_POLICY_TREE **ptree, STACK_OF(X509) *certs,
* Return value: 1 on success, 0 otherwise
*/
static int tree_link_matching_nodes(X509_POLICY_LEVEL *curr,
- X509_POLICY_DATA *data)
+ X509_POLICY_DATA *data,
+ X509_POLICY_TREE *tree)
{
X509_POLICY_LEVEL *last = curr - 1;
int i, matched = 0;
@@ -253,13 +269,13 @@ static int tree_link_matching_nodes(X509_POLICY_LEVEL *curr,
X509_POLICY_NODE *node = sk_X509_POLICY_NODE_value(last->nodes, i);
if (policy_node_match(last, node, data->valid_policy)) {
- if (level_add_node(curr, data, node, NULL) == NULL)
+ if (level_add_node(curr, data, node, tree, 0) == NULL)
return 0;
matched = 1;
}
}
if (!matched && last->anyPolicy) {
- if (level_add_node(curr, data, last->anyPolicy, NULL) == NULL)
+ if (level_add_node(curr, data, last->anyPolicy, tree, 0) == NULL)
return 0;
}
return 1;
@@ -272,7 +288,8 @@ static int tree_link_matching_nodes(X509_POLICY_LEVEL *curr,
* Return value: 1 on success, 0 otherwise.
*/
static int tree_link_nodes(X509_POLICY_LEVEL *curr,
- const X509_POLICY_CACHE *cache)
+ const X509_POLICY_CACHE *cache,
+ X509_POLICY_TREE *tree)
{
int i;
@@ -280,7 +297,7 @@ static int tree_link_nodes(X509_POLICY_LEVEL *curr,
X509_POLICY_DATA *data = sk_X509_POLICY_DATA_value(cache->data, i);
/* Look for matching nodes in previous level */
- if (!tree_link_matching_nodes(curr, data))
+ if (!tree_link_matching_nodes(curr, data, tree))
return 0;
}
return 1;
@@ -311,7 +328,7 @@ static int tree_add_unmatched(X509_POLICY_LEVEL *curr,
/* Curr may not have anyPolicy */
data->qualifier_set = cache->anyPolicy->qualifier_set;
data->flags |= POLICY_DATA_FLAG_SHARED_QUALIFIERS;
- if (level_add_node(curr, data, node, tree) == NULL) {
+ if (level_add_node(curr, data, node, tree, 1) == NULL) {
policy_data_free(data);
return 0;
}
@@ -373,7 +390,7 @@ static int tree_link_any(X509_POLICY_LEVEL *curr,
}
/* Finally add link to anyPolicy */
if (last->anyPolicy &&
- level_add_node(curr, cache->anyPolicy, last->anyPolicy, NULL) == NULL)
+ level_add_node(curr, cache->anyPolicy, last->anyPolicy, tree, 0) == NULL)
return 0;
return 1;
}
@@ -555,7 +572,7 @@ static int tree_calculate_user_set(X509_POLICY_TREE *tree,
extra->qualifier_set = anyPolicy->data->qualifier_set;
extra->flags = POLICY_DATA_FLAG_SHARED_QUALIFIERS
| POLICY_DATA_FLAG_EXTRA_NODE;
- node = level_add_node(NULL, extra, anyPolicy->parent, tree);
+ node = level_add_node(NULL, extra, anyPolicy->parent, tree, 1);
}
if (!tree->user_policies) {
tree->user_policies = sk_X509_POLICY_NODE_new_null();
@@ -582,7 +599,7 @@ static int tree_evaluate(X509_POLICY_TREE *tree)
for (i = 1; i < tree->nlevel; i++, curr++) {
cache = policy_cache_set(curr->cert);
- if (!tree_link_nodes(curr, cache))
+ if (!tree_link_nodes(curr, cache, tree))
return X509_PCY_TREE_INTERNAL;
if (!(curr->flags & X509_V_FLAG_INHIBIT_ANY)

51
CVE-2023-0465.patch Normal file
View File

@ -0,0 +1,51 @@
From b013765abfa80036dc779dd0e50602c57bb3bf95 Mon Sep 17 00:00:00 2001
From: Matt Caswell <matt@openssl.org>
Date: Tue, 7 Mar 2023 16:52:55 +0000
Subject: [PATCH] Ensure that EXFLAG_INVALID_POLICY is checked even in leaf
certs
Even though we check the leaf cert to confirm it is valid, we
later ignored the invalid flag and did not notice that the leaf
cert was bad.
Fixes: CVE-2023-0465
Reviewed-by: Hugo Landau <hlandau@openssl.org>
Reviewed-by: Tomas Mraz <tomas@openssl.org>
(Merged from https://github.com/openssl/openssl/pull/20588)
---
deps/openssl/openssl/crypto/x509/x509_vfy.c | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/deps/openssl/openssl/crypto/x509/x509_vfy.c b/deps/openssl/openssl/crypto/x509/x509_vfy.c
index 925fbb5412583..1dfe4f9f31a58 100644
--- a/deps/openssl/openssl/crypto/x509/x509_vfy.c
+++ b/deps/openssl/openssl/crypto/x509/x509_vfy.c
@@ -1649,18 +1649,25 @@ static int check_policy(X509_STORE_CTX *ctx)
}
/* Invalid or inconsistent extensions */
if (ret == X509_PCY_TREE_INVALID) {
- int i;
+ int i, cbcalled = 0;
/* Locate certificates with bad extensions and notify callback. */
- for (i = 1; i < sk_X509_num(ctx->chain); i++) {
+ for (i = 0; i < sk_X509_num(ctx->chain); i++) {
X509 *x = sk_X509_value(ctx->chain, i);
if (!(x->ex_flags & EXFLAG_INVALID_POLICY))
continue;
+ cbcalled = 1;
if (!verify_cb_cert(ctx, x, i,
X509_V_ERR_INVALID_POLICY_EXTENSION))
return 0;
}
+ if (!cbcalled) {
+ /* Should not be able to get here */
+ X509err(X509_F_CHECK_POLICY, ERR_R_INTERNAL_ERROR);
+ return 0;
+ }
+ /* The callback ignored the error so we return success */
return 1;
}
if (ret == X509_PCY_TREE_FAILURE) {

495
CVE-2023-44487.patch Normal file
View File

@ -0,0 +1,495 @@
From 72b4af6143681f528f1d237b21a9a7aee1738832 Mon Sep 17 00:00:00 2001
From: Tatsuhiro Tsujikawa <tatsuhiro.t@gmail.com>
Date: Sun, 1 Oct 2023 00:05:01 +0900
Subject: [PATCH] Rework session management
Origin:
https://github.com/nghttp2/nghttp2/commit/72b4af6143681f528f1d237b21a9a7aee1738832
---
deps/nghttp2/lib/Makefile.am | 4 ++
deps/nghttp2/lib/includes/nghttp2/nghttp2.h | 17 +++++
deps/nghttp2/lib/nghttp2_option.c | 7 ++
deps/nghttp2/lib/nghttp2_option.h | 6 ++
deps/nghttp2/lib/nghttp2_ratelim.c | 75 +++++++++++++++++++++
deps/nghttp2/lib/nghttp2_ratelim.h | 57 ++++++++++++++++
deps/nghttp2/lib/nghttp2_session.c | 34 +++++++++-
deps/nghttp2/lib/nghttp2_session.h | 12 +++-
deps/nghttp2/lib/nghttp2_time.c | 62 +++++++++++++++++
deps/nghttp2/lib/nghttp2_time.h | 38 +++++++++++
10 files changed, 310 insertions(+), 2 deletions(-)
create mode 100644 deps/nghttp2/lib/nghttp2_ratelim.c
create mode 100644 deps/nghttp2/lib/nghttp2_ratelim.h
create mode 100644 deps/nghttp2/lib/nghttp2_time.c
create mode 100644 deps/nghttp2/lib/nghttp2_time.h
diff --git a/deps/nghttp2/lib/Makefile.am b/deps/nghttp2/lib/Makefile.am
index 24a5bd62..595714d0 100644
--- a/deps/nghttp2/lib/Makefile.am
+++ b/deps/nghttp2/lib/Makefile.am
@@ -49,6 +49,8 @@ OBJECTS = nghttp2_pq.c nghttp2_map.c nghttp2_queue.c \
nghttp2_mem.c \
nghttp2_http.c \
nghttp2_rcbuf.c \
+ nghttp2_ratelim.c \
+ nghttp2_time.c \
nghttp2_debug.c
HFILES = nghttp2_pq.h nghttp2_int.h nghttp2_map.h nghttp2_queue.h \
@@ -65,6 +67,8 @@ HFILES = nghttp2_pq.h nghttp2_int.h nghttp2_map.h nghttp2_queue.h \
nghttp2_mem.h \
nghttp2_http.h \
nghttp2_rcbuf.h \
+ nghttp2_ratelim.h \
+ nghttp2_time.h \
nghttp2_debug.h
libnghttp2_la_SOURCES = $(HFILES) $(OBJECTS)
diff --git a/deps/nghttp2/lib/includes/nghttp2/nghttp2.h b/deps/nghttp2/lib/includes/nghttp2/nghttp2.h
index 9be6eea5..e0128cf5 100644
--- a/deps/nghttp2/lib/includes/nghttp2/nghttp2.h
+++ b/deps/nghttp2/lib/includes/nghttp2/nghttp2.h
@@ -2682,6 +2682,23 @@ NGHTTP2_EXTERN void nghttp2_option_set_max_outbound_ack(nghttp2_option *option,
NGHTTP2_EXTERN void nghttp2_option_set_max_settings(nghttp2_option *option,
size_t val);
+/**
+ * @function
+ *
+ * This function sets the rate limit for the incoming stream reset
+ * (RST_STREAM frame). It is server use only. It is a token-bucket
+ * based rate limiter. |burst| specifies the number of tokens that is
+ * initially available. The maximum number of tokens is capped to
+ * this value. |rate| specifies the number of tokens that are
+ * regenerated per second. An incoming RST_STREAM consumes one token.
+ * If there is no token available, GOAWAY is sent to tear down the
+ * connection. |burst| and |rate| default to 1000 and 33
+ * respectively.
+ */
+NGHTTP2_EXTERN void
+nghttp2_option_set_stream_reset_rate_limit(nghttp2_option *option,
+ uint64_t burst, uint64_t rate);
+
/**
* @function
*
diff --git a/deps/nghttp2/lib/nghttp2_option.c b/deps/nghttp2/lib/nghttp2_option.c
index 34348e66..0d9a4044 100644
--- a/deps/nghttp2/lib/nghttp2_option.c
+++ b/deps/nghttp2/lib/nghttp2_option.c
@@ -126,3 +126,10 @@ void nghttp2_option_set_max_settings(nghttp2_option *option, size_t val) {
option->opt_set_mask |= NGHTTP2_OPT_MAX_SETTINGS;
option->max_settings = val;
}
+
+void nghttp2_option_set_stream_reset_rate_limit(nghttp2_option *option,
+ uint64_t burst, uint64_t rate) {
+ option->opt_set_mask |= NGHTTP2_OPT_STREAM_RESET_RATE_LIMIT;
+ option->stream_reset_burst = burst;
+ option->stream_reset_rate = rate;
+}
diff --git a/deps/nghttp2/lib/nghttp2_option.h b/deps/nghttp2/lib/nghttp2_option.h
index 939729fd..e6ba9100 100644
--- a/deps/nghttp2/lib/nghttp2_option.h
+++ b/deps/nghttp2/lib/nghttp2_option.h
@@ -68,12 +68,18 @@ typedef enum {
NGHTTP2_OPT_NO_CLOSED_STREAMS = 1 << 10,
NGHTTP2_OPT_MAX_OUTBOUND_ACK = 1 << 11,
NGHTTP2_OPT_MAX_SETTINGS = 1 << 12,
+ NGHTTP2_OPT_STREAM_RESET_RATE_LIMIT = 1 << 15,
} nghttp2_option_flag;
/**
* Struct to store option values for nghttp2_session.
*/
struct nghttp2_option {
+ /**
+ * NGHTTP2_OPT_STREAM_RESET_RATE_LIMIT
+ */
+ uint64_t stream_reset_burst;
+ uint64_t stream_reset_rate;
/**
* NGHTTP2_OPT_MAX_SEND_HEADER_BLOCK_LENGTH
*/
diff --git a/deps/nghttp2/lib/nghttp2_ratelim.c b/deps/nghttp2/lib/nghttp2_ratelim.c
new file mode 100644
index 00000000..7011655b
--- /dev/null
+++ b/deps/nghttp2/lib/nghttp2_ratelim.c
@@ -0,0 +1,75 @@
+/*
+ * nghttp2 - HTTP/2 C Library
+ *
+ * Copyright (c) 2023 nghttp2 contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+#include "nghttp2_ratelim.h"
+#include "nghttp2_helper.h"
+
+void nghttp2_ratelim_init(nghttp2_ratelim *rl, uint64_t burst, uint64_t rate) {
+ rl->val = rl->burst = burst;
+ rl->rate = rate;
+ rl->tstamp = 0;
+}
+
+void nghttp2_ratelim_update(nghttp2_ratelim *rl, uint64_t tstamp) {
+ uint64_t d, gain;
+
+ if (tstamp == rl->tstamp) {
+ return;
+ }
+
+ if (tstamp > rl->tstamp) {
+ d = tstamp - rl->tstamp;
+ } else {
+ d = 1;
+ }
+
+ rl->tstamp = tstamp;
+
+ if (UINT64_MAX / d < rl->rate) {
+ rl->val = rl->burst;
+
+ return;
+ }
+
+ gain = rl->rate * d;
+
+ if (UINT64_MAX - gain < rl->val) {
+ rl->val = rl->burst;
+
+ return;
+ }
+
+ rl->val += gain;
+ rl->val = nghttp2_min(rl->val, rl->burst);
+}
+
+int nghttp2_ratelim_drain(nghttp2_ratelim *rl, uint64_t n) {
+ if (rl->val < n) {
+ return -1;
+ }
+
+ rl->val -= n;
+
+ return 0;
+}
diff --git a/deps/nghttp2/lib/nghttp2_ratelim.h b/deps/nghttp2/lib/nghttp2_ratelim.h
new file mode 100644
index 00000000..866ed3f0
--- /dev/null
+++ b/deps/nghttp2/lib/nghttp2_ratelim.h
@@ -0,0 +1,57 @@
+/*
+ * nghttp2 - HTTP/2 C Library
+ *
+ * Copyright (c) 2023 nghttp2 contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+#ifndef NGHTTP2_RATELIM_H
+#define NGHTTP2_RATELIM_H
+
+#ifdef HAVE_CONFIG_H
+# include <config.h>
+#endif /* HAVE_CONFIG_H */
+
+#include <nghttp2/nghttp2.h>
+
+typedef struct nghttp2_ratelim {
+ /* burst is the maximum value of val. */
+ uint64_t burst;
+ /* rate is the amount of value that is regenerated per 1 tstamp. */
+ uint64_t rate;
+ /* val is the amount of value available to drain. */
+ uint64_t val;
+ /* tstamp is the last timestamp in second resolution that is known
+ to this object. */
+ uint64_t tstamp;
+} nghttp2_ratelim;
+
+/* nghttp2_ratelim_init initializes |rl| with the given parameters. */
+void nghttp2_ratelim_init(nghttp2_ratelim *rl, uint64_t burst, uint64_t rate);
+
+/* nghttp2_ratelim_update updates rl->val with the current |tstamp|
+ given in second resolution. */
+void nghttp2_ratelim_update(nghttp2_ratelim *rl, uint64_t tstamp);
+
+/* nghttp2_ratelim_drain drains |n| from rl->val. It returns 0 if it
+ succeeds, or -1. */
+int nghttp2_ratelim_drain(nghttp2_ratelim *rl, uint64_t n);
+
+#endif /* NGHTTP2_RATELIM_H */
diff --git a/deps/nghttp2/lib/nghttp2_session.c b/deps/nghttp2/lib/nghttp2_session.c
index 39f81f49..3f5f76f2 100644
--- a/deps/nghttp2/lib/nghttp2_session.c
+++ b/deps/nghttp2/lib/nghttp2_session.c
@@ -36,6 +36,7 @@
#include "nghttp2_option.h"
#include "nghttp2_http.h"
#include "nghttp2_pq.h"
+#include "nghttp2_time.h"
#include "nghttp2_debug.h"
/*
@@ -443,6 +444,10 @@ static int session_new(nghttp2_session **session_ptr,
NGHTTP2_DEFAULT_MAX_CONCURRENT_STREAMS;
(*session_ptr)->pending_enable_push = 1;
+ nghttp2_ratelim_init(&(*session_ptr)->stream_reset_ratelim,
+ NGHTTP2_DEFAULT_STREAM_RESET_BURST,
+ NGHTTP2_DEFAULT_STREAM_RESET_RATE);
+
if (server) {
(*session_ptr)->server = 1;
}
@@ -527,6 +532,12 @@ static int session_new(nghttp2_session **session_ptr,
option->max_settings) {
(*session_ptr)->max_settings = option->max_settings;
}
+
+ if (option->opt_set_mask & NGHTTP2_OPT_STREAM_RESET_RATE_LIMIT) {
+ nghttp2_ratelim_init(&(*session_ptr)->stream_reset_ratelim,
+ option->stream_reset_burst,
+ option->stream_reset_rate);
+ }
}
rv = nghttp2_hd_deflate_init2(&(*session_ptr)->hd_deflater,
@@ -4142,6 +4153,23 @@ static int session_process_priority_frame(nghttp2_session *session) {
return nghttp2_session_on_priority_received(session, frame);
}
+static int session_update_stream_reset_ratelim(nghttp2_session *session) {
+ if (!session->server || (session->goaway_flags & NGHTTP2_GOAWAY_SUBMITTED)) {
+ return 0;
+ }
+
+ nghttp2_ratelim_update(&session->stream_reset_ratelim,
+ nghttp2_time_now_sec());
+
+ if (nghttp2_ratelim_drain(&session->stream_reset_ratelim, 1) == 0) {
+ return 0;
+ }
+
+ return nghttp2_session_add_goaway(session, session->last_recv_stream_id,
+ NGHTTP2_INTERNAL_ERROR, NULL, 0,
+ NGHTTP2_GOAWAY_AUX_NONE);
+}
+
int nghttp2_session_on_rst_stream_received(nghttp2_session *session,
nghttp2_frame *frame) {
int rv;
@@ -4171,7 +4199,8 @@ int nghttp2_session_on_rst_stream_received(nghttp2_session *session,
if (nghttp2_is_fatal(rv)) {
return rv;
}
- return 0;
+
+ return session_update_stream_reset_ratelim(session);
}
static int session_process_rst_stream_frame(nghttp2_session *session) {
@@ -6973,6 +7002,9 @@ int nghttp2_session_add_window_update(nghttp2_session *session, uint8_t flags,
nghttp2_mem_free(mem, item);
return rv;
}
+
+ session->goaway_flags |= NGHTTP2_GOAWAY_SUBMITTED;
+
return 0;
}
diff --git a/deps/nghttp2/lib/nghttp2_session.h b/deps/nghttp2/lib/nghttp2_session.h
index 07bfbb6c..9d429921 100644
--- a/deps/nghttp2/lib/nghttp2_session.h
+++ b/deps/nghttp2/lib/nghttp2_session.h
@@ -39,6 +39,7 @@
#include "nghttp2_buf.h"
#include "nghttp2_callbacks.h"
#include "nghttp2_mem.h"
+#include "nghttp2_ratelim.h"
/* The global variable for tests where we want to disable strict
preface handling. */
@@ -102,6 +103,10 @@ typedef struct {
/* The default value of maximum number of concurrent streams. */
#define NGHTTP2_DEFAULT_MAX_CONCURRENT_STREAMS 0xffffffffu
+/* The default values for stream reset rate limiter. */
+#define NGHTTP2_DEFAULT_STREAM_RESET_BURST 1000
+#define NGHTTP2_DEFAULT_STREAM_RESET_RATE 33
+
/* Internal state when receiving incoming frame */
typedef enum {
/* Receiving frame header */
@@ -176,7 +181,9 @@ typedef enum {
/* Flag means GOAWAY was sent */
NGHTTP2_GOAWAY_SENT = 0x4,
/* Flag means GOAWAY was received */
- NGHTTP2_GOAWAY_RECV = 0x8
+ NGHTTP2_GOAWAY_RECV = 0x8,
+ /* Flag means GOAWAY has been submitted at least once */
+ NGHTTP2_GOAWAY_SUBMITTED = 0x10
} nghttp2_goaway_flag;
/* nghttp2_inflight_settings stores the SETTINGS entries which local
@@ -227,6 +234,9 @@ struct nghttp2_session {
/* Queue of In-flight SETTINGS values. SETTINGS bearing ACK is not
considered as in-flight. */
nghttp2_inflight_settings *inflight_settings_head;
+ /* Stream reset rate limiter. If receiving excessive amount of
+ stream resets, GOAWAY will be sent. */
+ nghttp2_ratelim stream_reset_ratelim;
/* The number of outgoing streams. This will be capped by
remote_settings.max_concurrent_streams. */
size_t num_outgoing_streams;
diff --git a/deps/nghttp2/lib/nghttp2_time.c b/deps/nghttp2/lib/nghttp2_time.c
new file mode 100644
index 00000000..2a5f1a6f
--- /dev/null
+++ b/deps/nghttp2/lib/nghttp2_time.c
@@ -0,0 +1,62 @@
+/*
+ * nghttp2 - HTTP/2 C Library
+ *
+ * Copyright (c) 2023 nghttp2 contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+#include "nghttp2_time.h"
+
+#ifdef HAVE_TIME_H
+# include <time.h>
+#endif /* HAVE_TIME_H */
+
+#ifdef HAVE_SYSINFOAPI_H
+# include <sysinfoapi.h>
+#endif /* HAVE_SYSINFOAPI_H */
+
+#ifndef HAVE_GETTICKCOUNT64
+static uint64_t time_now_sec(void) {
+ time_t t = time(NULL);
+
+ if (t == -1) {
+ return 0;
+ }
+
+ return (uint64_t)t;
+}
+#endif /* HAVE_GETTICKCOUNT64 */
+
+#ifdef HAVE_CLOCK_GETTIME
+uint64_t nghttp2_time_now_sec(void) {
+ struct timespec tp;
+ int rv = clock_gettime(CLOCK_MONOTONIC, &tp);
+
+ if (rv == -1) {
+ return time_now_sec();
+ }
+
+ return (uint64_t)tp.tv_sec;
+}
+#elif defined(HAVE_GETTICKCOUNT64)
+uint64_t nghttp2_time_now_sec(void) { return GetTickCount64() / 1000; }
+#else /* !HAVE_CLOCK_GETTIME && !HAVE_GETTICKCOUNT64 */
+uint64_t nghttp2_time_now_sec(void) { return time_now_sec(); }
+#endif /* !HAVE_CLOCK_GETTIME && !HAVE_GETTICKCOUNT64 */
diff --git a/deps/nghttp2/lib/nghttp2_time.h b/deps/nghttp2/lib/nghttp2_time.h
new file mode 100644
index 00000000..03c0bbe9
--- /dev/null
+++ b/deps/nghttp2/lib/nghttp2_time.h
@@ -0,0 +1,38 @@
+/*
+ * nghttp2 - HTTP/2 C Library
+ *
+ * Copyright (c) 2023 nghttp2 contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+#ifndef NGHTTP2_TIME_H
+#define NGHTTP2_TIME_H
+
+#ifdef HAVE_CONFIG_H
+# include <config.h>
+#endif /* HAVE_CONFIG_H */
+
+#include <nghttp2/nghttp2.h>
+
+/* nghttp2_time_now_sec returns seconds from implementation-specific
+ timepoint. If it is unable to get seconds, it returns 0. */
+uint64_t nghttp2_time_now_sec(void);
+
+#endif /* NGHTTP2_TIME_H */
--
2.33.0

615
CVE-2023-46809.patch Normal file
View File

@ -0,0 +1,615 @@
From: Michael Dawson <midawson@redhat.com>
Date: Thu, 4 Jan 2024 21:32:51 +0000
Subject: CVE-2023-46809 crypto: disable PKCS#1 padding for privateDecrypt
Refs: https://hackerone.com/bugs?subject=nodejs&report_id=2269177
Disable RSA_PKCS1_PADDING for crypto.privateDecrypt() in order
to protect against the Marvin attack.
Includes a security revert flag that can be used to restore
support.
Signed-off-by: Michael Dawson <midawson@redhat.com>
bug-github-pull: https://github.com/nodejs-private/node-private/pull/525
bug-hakerone: https://hackerone.com/bugs?subject=nodejs&report_id=2269177
bug: https://nodejs.org/en/blog/vulnerability/february-2024-security-releases/#nodejs-is-vulnerable-to-the-marvin-attack-timing-variant-of-the-bleichenbacher-attack-against-pkcs1
origin: backport, https://github.com/nodejs/node/commit/d3d357ab096884f10f5d2f164149727eea875635
Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
CVE-ID: CVE-2023-46809
---
src/node_crypto.cc | 28 ++
src/node_revert.h | 4 +-
test/parallel/test-crypto-rsa-dsa-revert.js | 466 ++++++++++++++++++++++++++++
test/parallel/test-crypto-rsa-dsa.js | 42 ++-
4 files changed, 525 insertions(+), 15 deletions(-)
create mode 100644 test/parallel/test-crypto-rsa-dsa-revert.js
diff --git a/src/node_crypto.cc b/src/node_crypto.cc
index 6c2a5de..91d52c5 100644
--- a/src/node_crypto.cc
+++ b/src/node_crypto.cc
@@ -37,6 +37,7 @@
#include "string_bytes.h"
#include "threadpoolwork-inl.h"
#include "util-inl.h"
+#include "node_revert.h"
#include "v8.h"
#include <openssl/ec.h>
@@ -5111,6 +5112,33 @@ void PublicKeyCipher::Cipher(const FunctionCallbackInfo<Value>& args) {
uint32_t padding;
if (!args[offset + 1]->Uint32Value(env->context()).To(&padding)) return;
+ if (EVP_PKEY_cipher == EVP_PKEY_decrypt &&
+ operation == PublicKeyCipher::kPrivate && padding == RSA_PKCS1_PADDING &&
+ !IsReverted(SECURITY_REVERT_CVE_2023_46809)) {
+ EVPKeyCtxPointer ctx(EVP_PKEY_CTX_new(pkey.get(), nullptr));
+ CHECK(ctx);
+
+ if (EVP_PKEY_decrypt_init(ctx.get()) <= 0) {
+ return ThrowCryptoError(env, ERR_get_error());
+ }
+
+ int rsa_pkcs1_implicit_rejection =
+ EVP_PKEY_CTX_ctrl_str(ctx.get(), "rsa_pkcs1_implicit_rejection", "1");
+ // From the doc -2 means that the option is not supported.
+ // The default for the option is enabled and if it has been
+ // specifically disabled we want to respect that so we will
+ // not throw an error if the option is supported regardless
+ // of how it is set. The call to set the value
+ // will not affect what is used since a different context is
+ // used in the call if the option is supported
+ if (rsa_pkcs1_implicit_rejection <= 0) {
+ return THROW_ERR_INVALID_ARG_VALUE(
+ env,
+ "RSA_PKCS1_PADDING is no longer supported for private decryption,"
+ " this can be reverted with --security-revert=CVE-2023-46809");
+ }
+ }
+
const node::Utf8Value oaep_str(env->isolate(), args[offset + 2]);
const char* oaep_hash = args[offset + 2]->IsString() ? *oaep_str : nullptr;
const EVP_MD* digest = nullptr;
diff --git a/src/node_revert.h b/src/node_revert.h
index b95eb0d..c636720 100644
--- a/src/node_revert.h
+++ b/src/node_revert.h
@@ -22,9 +22,7 @@ namespace node {
XX(CVE_2019_9518, "CVE-2019-9518", "HTTP/2 Empty DATA Frame Flooding") \
XX(CVE_2021_44531, "CVE-2021-44531", "Cert Verif Bypass via URI SAN") \
XX(CVE_2021_44532, "CVE-2021-44532", "Cert Verif Bypass via Str Inject") \
-// XX(CVE_2016_PEND, "CVE-2016-PEND", "Vulnerability Title")
- // TODO(addaleax): Remove all of the above before Node.js 13 as the comment
- // at the start of the file indicates.
+ XX(CVE_2023_46809, "CVE-2023-46809", "Marvin attack on PKCS#1 padding")
enum reversion {
#define V(code, ...) SECURITY_REVERT_##code,
diff --git a/test/parallel/test-crypto-rsa-dsa-revert.js b/test/parallel/test-crypto-rsa-dsa-revert.js
new file mode 100644
index 0000000..d40c66f
--- /dev/null
+++ b/test/parallel/test-crypto-rsa-dsa-revert.js
@@ -0,0 +1,466 @@
+'use strict';
+// Flags: --security-revert=CVE-2023-46809
+const common = require('../common');
+if (!common.hasCrypto)
+ common.skip('missing crypto');
+
+const assert = require('assert');
+const crypto = require('crypto');
+
+const constants = crypto.constants;
+
+const fixtures = require('../common/fixtures');
+
+// Test certificates
+const certPem = fixtures.readKey('rsa_cert.crt');
+const keyPem = fixtures.readKey('rsa_private.pem');
+const rsaKeySize = 2048;
+const rsaPubPem = fixtures.readKey('rsa_public.pem', 'ascii');
+const rsaKeyPem = fixtures.readKey('rsa_private.pem', 'ascii');
+const rsaKeyPemEncrypted = fixtures.readKey('rsa_private_encrypted.pem',
+ 'ascii');
+const dsaPubPem = fixtures.readKey('dsa_public.pem', 'ascii');
+const dsaKeyPem = fixtures.readKey('dsa_private.pem', 'ascii');
+const dsaKeyPemEncrypted = fixtures.readKey('dsa_private_encrypted.pem',
+ 'ascii');
+const rsaPkcs8KeyPem = fixtures.readKey('rsa_private_pkcs8.pem');
+const dsaPkcs8KeyPem = fixtures.readKey('dsa_private_pkcs8.pem');
+
+const ec = new TextEncoder();
+
+const openssl1DecryptError = {
+ message: 'error:06065064:digital envelope routines:EVP_DecryptFinal_ex:' +
+ 'bad decrypt',
+ code: 'ERR_OSSL_EVP_BAD_DECRYPT',
+ reason: 'bad decrypt',
+ function: 'EVP_DecryptFinal_ex',
+ library: 'digital envelope routines',
+};
+
+const decryptError = common.hasOpenSSL3 ?
+ { message: 'error:1C800064:Provider routines::bad decrypt' } :
+ openssl1DecryptError;
+
+const decryptPrivateKeyError = common.hasOpenSSL3 ? {
+ message: 'error:1C800064:Provider routines::bad decrypt',
+} : openssl1DecryptError;
+
+function getBufferCopy(buf) {
+ return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
+}
+
+// Test RSA encryption/decryption
+{
+ const input = 'I AM THE WALRUS';
+ const bufferToEncrypt = Buffer.from(input);
+ const bufferPassword = Buffer.from('password');
+
+ let encryptedBuffer = crypto.publicEncrypt(rsaPubPem, bufferToEncrypt);
+
+ // Test other input types
+ let otherEncrypted;
+ {
+ const ab = getBufferCopy(ec.encode(rsaPubPem));
+ const ab2enc = getBufferCopy(bufferToEncrypt);
+
+ crypto.publicEncrypt(ec.encode(rsaPubPem), bufferToEncrypt);
+ crypto.publicEncrypt(new Uint8Array(ab), new Uint8Array(ab2enc));
+ crypto.publicEncrypt(new DataView(ab), new DataView(ab2enc));
+ otherEncrypted = crypto.publicEncrypt({
+ key: Buffer.from(ab)
+ }, Buffer.from(ab2enc));
+ }
+
+ let decryptedBuffer = crypto.privateDecrypt(rsaKeyPem, encryptedBuffer);
+ const otherDecrypted = crypto.privateDecrypt(rsaKeyPem, otherEncrypted);
+ assert.strictEqual(decryptedBuffer.toString(), input);
+ assert.strictEqual(otherDecrypted.toString(), input);
+
+ decryptedBuffer = crypto.privateDecrypt(rsaPkcs8KeyPem, encryptedBuffer);
+ assert.strictEqual(decryptedBuffer.toString(), input);
+
+ let decryptedBufferWithPassword = crypto.privateDecrypt({
+ key: rsaKeyPemEncrypted,
+ passphrase: 'password'
+ }, encryptedBuffer);
+
+ const otherDecryptedBufferWithPassword = crypto.privateDecrypt({
+ key: rsaKeyPemEncrypted,
+ passphrase: ec.encode('password')
+ }, encryptedBuffer);
+
+ assert.strictEqual(
+ otherDecryptedBufferWithPassword.toString(),
+ decryptedBufferWithPassword.toString());
+
+ decryptedBufferWithPassword = crypto.privateDecrypt({
+ key: rsaKeyPemEncrypted,
+ passphrase: 'password'
+ }, encryptedBuffer);
+
+ assert.strictEqual(decryptedBufferWithPassword.toString(), input);
+
+ encryptedBuffer = crypto.publicEncrypt({
+ key: rsaKeyPemEncrypted,
+ passphrase: 'password'
+ }, bufferToEncrypt);
+
+ decryptedBufferWithPassword = crypto.privateDecrypt({
+ key: rsaKeyPemEncrypted,
+ passphrase: 'password'
+ }, encryptedBuffer);
+ assert.strictEqual(decryptedBufferWithPassword.toString(), input);
+
+ encryptedBuffer = crypto.privateEncrypt({
+ key: rsaKeyPemEncrypted,
+ passphrase: bufferPassword
+ }, bufferToEncrypt);
+
+ decryptedBufferWithPassword = crypto.publicDecrypt({
+ key: rsaKeyPemEncrypted,
+ passphrase: bufferPassword
+ }, encryptedBuffer);
+ assert.strictEqual(decryptedBufferWithPassword.toString(), input);
+
+ // Now with explicit RSA_PKCS1_PADDING.
+ encryptedBuffer = crypto.privateEncrypt({
+ padding: crypto.constants.RSA_PKCS1_PADDING,
+ key: rsaKeyPemEncrypted,
+ passphrase: bufferPassword
+ }, bufferToEncrypt);
+
+ decryptedBufferWithPassword = crypto.publicDecrypt({
+ padding: crypto.constants.RSA_PKCS1_PADDING,
+ key: rsaKeyPemEncrypted,
+ passphrase: bufferPassword
+ }, encryptedBuffer);
+ assert.strictEqual(decryptedBufferWithPassword.toString(), input);
+
+ // Omitting padding should be okay because RSA_PKCS1_PADDING is the default.
+ decryptedBufferWithPassword = crypto.publicDecrypt({
+ key: rsaKeyPemEncrypted,
+ passphrase: bufferPassword
+ }, encryptedBuffer);
+ assert.strictEqual(decryptedBufferWithPassword.toString(), input);
+
+ // Now with RSA_NO_PADDING. Plaintext needs to match key size.
+ // OpenSSL 3.x has a rsa_check_padding that will cause an error if
+ // RSA_NO_PADDING is used.
+ if (!common.hasOpenSSL3) {
+ {
+ const plaintext = 'x'.repeat(rsaKeySize / 8);
+ encryptedBuffer = crypto.privateEncrypt({
+ padding: crypto.constants.RSA_NO_PADDING,
+ key: rsaKeyPemEncrypted,
+ passphrase: bufferPassword
+ }, Buffer.from(plaintext));
+
+ decryptedBufferWithPassword = crypto.publicDecrypt({
+ padding: crypto.constants.RSA_NO_PADDING,
+ key: rsaKeyPemEncrypted,
+ passphrase: bufferPassword
+ }, encryptedBuffer);
+ assert.strictEqual(decryptedBufferWithPassword.toString(), plaintext);
+ }
+ }
+
+ encryptedBuffer = crypto.publicEncrypt(certPem, bufferToEncrypt);
+
+ decryptedBuffer = crypto.privateDecrypt(keyPem, encryptedBuffer);
+ assert.strictEqual(decryptedBuffer.toString(), input);
+
+ encryptedBuffer = crypto.publicEncrypt(keyPem, bufferToEncrypt);
+
+ decryptedBuffer = crypto.privateDecrypt(keyPem, encryptedBuffer);
+ assert.strictEqual(decryptedBuffer.toString(), input);
+
+ encryptedBuffer = crypto.privateEncrypt(keyPem, bufferToEncrypt);
+
+ decryptedBuffer = crypto.publicDecrypt(keyPem, encryptedBuffer);
+ assert.strictEqual(decryptedBuffer.toString(), input);
+
+ assert.throws(() => {
+ crypto.privateDecrypt({
+ key: rsaKeyPemEncrypted,
+ passphrase: 'wrong'
+ }, bufferToEncrypt);
+ }, decryptError);
+
+ assert.throws(() => {
+ crypto.publicEncrypt({
+ key: rsaKeyPemEncrypted,
+ passphrase: 'wrong'
+ }, encryptedBuffer);
+ }, decryptError);
+
+ encryptedBuffer = crypto.privateEncrypt({
+ key: rsaKeyPemEncrypted,
+ passphrase: Buffer.from('password')
+ }, bufferToEncrypt);
+
+ assert.throws(() => {
+ crypto.publicDecrypt({
+ key: rsaKeyPemEncrypted,
+ passphrase: Buffer.from('wrong')
+ }, encryptedBuffer);
+ }, decryptError);
+}
+
+function test_rsa(padding, encryptOaepHash, decryptOaepHash) {
+ const size = (padding === 'RSA_NO_PADDING') ? rsaKeySize / 8 : 32;
+ const input = Buffer.allocUnsafe(size);
+ for (let i = 0; i < input.length; i++)
+ input[i] = (i * 7 + 11) & 0xff;
+ const bufferToEncrypt = Buffer.from(input);
+
+ padding = constants[padding];
+
+ const encryptedBuffer = crypto.publicEncrypt({
+ key: rsaPubPem,
+ padding: padding,
+ oaepHash: encryptOaepHash
+ }, bufferToEncrypt);
+
+ let decryptedBuffer = crypto.privateDecrypt({
+ key: rsaKeyPem,
+ padding: padding,
+ oaepHash: decryptOaepHash
+ }, encryptedBuffer);
+ assert.deepStrictEqual(decryptedBuffer, input);
+
+ decryptedBuffer = crypto.privateDecrypt({
+ key: rsaPkcs8KeyPem,
+ padding: padding,
+ oaepHash: decryptOaepHash
+ }, encryptedBuffer);
+ assert.deepStrictEqual(decryptedBuffer, input);
+}
+
+test_rsa('RSA_NO_PADDING');
+test_rsa('RSA_PKCS1_PADDING');
+test_rsa('RSA_PKCS1_OAEP_PADDING');
+
+// Test OAEP with different hash functions.
+test_rsa('RSA_PKCS1_OAEP_PADDING', undefined, 'sha1');
+test_rsa('RSA_PKCS1_OAEP_PADDING', 'sha1', undefined);
+test_rsa('RSA_PKCS1_OAEP_PADDING', 'sha256', 'sha256');
+test_rsa('RSA_PKCS1_OAEP_PADDING', 'sha512', 'sha512');
+assert.throws(() => {
+ test_rsa('RSA_PKCS1_OAEP_PADDING', 'sha256', 'sha512');
+}, {
+ code: 'ERR_OSSL_RSA_OAEP_DECODING_ERROR'
+});
+
+// The following RSA-OAEP test cases were created using the WebCrypto API to
+// ensure compatibility when using non-SHA1 hash functions.
+{
+ const { decryptionTests } =
+ JSON.parse(fixtures.readSync('rsa-oaep-test-vectors.js', 'utf8'));
+
+ for (const { ct, oaepHash, oaepLabel } of decryptionTests) {
+ const label = oaepLabel ? Buffer.from(oaepLabel, 'hex') : undefined;
+ const copiedLabel = oaepLabel ? getBufferCopy(label) : undefined;
+
+ const decrypted = crypto.privateDecrypt({
+ key: rsaPkcs8KeyPem,
+ oaepHash,
+ oaepLabel: oaepLabel ? label : undefined
+ }, Buffer.from(ct, 'hex'));
+
+ assert.strictEqual(decrypted.toString('utf8'), 'Hello Node.js');
+ }
+}
+
+// Test invalid oaepHash and oaepLabel options.
+for (const fn of [crypto.publicEncrypt, crypto.privateDecrypt]) {
+ assert.throws(() => {
+ fn({
+ key: rsaPubPem,
+ oaepHash: 'Hello world'
+ }, Buffer.alloc(10));
+ }, {
+ code: 'ERR_OSSL_EVP_INVALID_DIGEST'
+ });
+
+ for (const oaepHash of [0, false, null, Symbol(), () => {}]) {
+ assert.throws(() => {
+ fn({
+ key: rsaPubPem,
+ oaepHash
+ }, Buffer.alloc(10));
+ }, {
+ code: 'ERR_INVALID_ARG_TYPE'
+ });
+ }
+
+ for (const oaepLabel of [0, false, null, Symbol(), () => {}, {}]) {
+ assert.throws(() => {
+ fn({
+ key: rsaPubPem,
+ oaepLabel
+ }, Buffer.alloc(10));
+ }, {
+ code: 'ERR_INVALID_ARG_TYPE'
+ });
+ }
+}
+
+// Test RSA key signing/verification
+let rsaSign = crypto.createSign('SHA1');
+let rsaVerify = crypto.createVerify('SHA1');
+assert.ok(rsaSign);
+assert.ok(rsaVerify);
+
+const expectedSignature = fixtures.readKey(
+ 'rsa_public_sha1_signature_signedby_rsa_private_pkcs8.sha1',
+ 'hex'
+);
+
+rsaSign.update(rsaPubPem);
+let rsaSignature = rsaSign.sign(rsaKeyPem, 'hex');
+assert.strictEqual(rsaSignature, expectedSignature);
+
+rsaVerify.update(rsaPubPem);
+assert.strictEqual(rsaVerify.verify(rsaPubPem, rsaSignature, 'hex'), true);
+
+// Test RSA PKCS#8 key signing/verification
+rsaSign = crypto.createSign('SHA1');
+rsaSign.update(rsaPubPem);
+rsaSignature = rsaSign.sign(rsaPkcs8KeyPem, 'hex');
+assert.strictEqual(rsaSignature, expectedSignature);
+
+rsaVerify = crypto.createVerify('SHA1');
+rsaVerify.update(rsaPubPem);
+assert.strictEqual(rsaVerify.verify(rsaPubPem, rsaSignature, 'hex'), true);
+
+// Test RSA key signing/verification with encrypted key
+rsaSign = crypto.createSign('SHA1');
+rsaSign.update(rsaPubPem);
+const signOptions = { key: rsaKeyPemEncrypted, passphrase: 'password' };
+rsaSignature = rsaSign.sign(signOptions, 'hex');
+assert.strictEqual(rsaSignature, expectedSignature);
+
+rsaVerify = crypto.createVerify('SHA1');
+rsaVerify.update(rsaPubPem);
+assert.strictEqual(rsaVerify.verify(rsaPubPem, rsaSignature, 'hex'), true);
+
+rsaSign = crypto.createSign('SHA1');
+rsaSign.update(rsaPubPem);
+assert.throws(() => {
+ const signOptions = { key: rsaKeyPemEncrypted, passphrase: 'wrong' };
+ rsaSign.sign(signOptions, 'hex');
+}, decryptPrivateKeyError);
+
+//
+// Test RSA signing and verification
+//
+{
+ const privateKey = fixtures.readKey('rsa_private_b.pem');
+ const publicKey = fixtures.readKey('rsa_public_b.pem');
+
+ const input = 'I AM THE WALRUS';
+
+ const signature = fixtures.readKey(
+ 'I_AM_THE_WALRUS_sha256_signature_signedby_rsa_private_b.sha256',
+ 'hex'
+ );
+
+ const sign = crypto.createSign('SHA256');
+ sign.update(input);
+
+ const output = sign.sign(privateKey, 'hex');
+ assert.strictEqual(output, signature);
+
+ const verify = crypto.createVerify('SHA256');
+ verify.update(input);
+
+ assert.strictEqual(verify.verify(publicKey, signature, 'hex'), true);
+
+ // Test the legacy signature algorithm name.
+ const sign2 = crypto.createSign('RSA-SHA256');
+ sign2.update(input);
+
+ const output2 = sign2.sign(privateKey, 'hex');
+ assert.strictEqual(output2, signature);
+
+ const verify2 = crypto.createVerify('SHA256');
+ verify2.update(input);
+
+ assert.strictEqual(verify2.verify(publicKey, signature, 'hex'), true);
+}
+
+
+//
+// Test DSA signing and verification
+//
+{
+ const input = 'I AM THE WALRUS';
+
+ // DSA signatures vary across runs so there is no static string to verify
+ // against.
+ const sign = crypto.createSign('SHA1');
+ sign.update(input);
+ const signature = sign.sign(dsaKeyPem, 'hex');
+
+ const verify = crypto.createVerify('SHA1');
+ verify.update(input);
+
+ assert.strictEqual(verify.verify(dsaPubPem, signature, 'hex'), true);
+
+ // Test the legacy 'DSS1' name.
+ const sign2 = crypto.createSign('DSS1');
+ sign2.update(input);
+ const signature2 = sign2.sign(dsaKeyPem, 'hex');
+
+ const verify2 = crypto.createVerify('DSS1');
+ verify2.update(input);
+
+ assert.strictEqual(verify2.verify(dsaPubPem, signature2, 'hex'), true);
+}
+
+
+//
+// Test DSA signing and verification with PKCS#8 private key
+//
+{
+ const input = 'I AM THE WALRUS';
+
+ // DSA signatures vary across runs so there is no static string to verify
+ // against.
+ const sign = crypto.createSign('SHA1');
+ sign.update(input);
+ const signature = sign.sign(dsaPkcs8KeyPem, 'hex');
+
+ const verify = crypto.createVerify('SHA1');
+ verify.update(input);
+
+ assert.strictEqual(verify.verify(dsaPubPem, signature, 'hex'), true);
+}
+
+
+//
+// Test DSA signing and verification with encrypted key
+//
+const input = 'I AM THE WALRUS';
+
+{
+ const sign = crypto.createSign('SHA1');
+ sign.update(input);
+ assert.throws(() => {
+ sign.sign({ key: dsaKeyPemEncrypted, passphrase: 'wrong' }, 'hex');
+ }, decryptPrivateKeyError);
+}
+
+{
+ // DSA signatures vary across runs so there is no static string to verify
+ // against.
+ const sign = crypto.createSign('SHA1');
+ sign.update(input);
+ const signOptions = { key: dsaKeyPemEncrypted, passphrase: 'password' };
+ const signature = sign.sign(signOptions, 'hex');
+
+ const verify = crypto.createVerify('SHA1');
+ verify.update(input);
+
+ assert.strictEqual(verify.verify(dsaPubPem, signature, 'hex'), true);
+}
diff --git a/test/parallel/test-crypto-rsa-dsa.js b/test/parallel/test-crypto-rsa-dsa.js
index 9b8c3f6..b45bfeb 100644
--- a/test/parallel/test-crypto-rsa-dsa.js
+++ b/test/parallel/test-crypto-rsa-dsa.js
@@ -169,19 +169,37 @@ function test_rsa(padding, encryptOaepHash, decryptOaepHash) {
oaepHash: encryptOaepHash
}, bufferToEncrypt);
- let decryptedBuffer = crypto.privateDecrypt({
- key: rsaKeyPem,
- padding: padding,
- oaepHash: decryptOaepHash
- }, encryptedBuffer);
- assert.deepStrictEqual(decryptedBuffer, input);
- decryptedBuffer = crypto.privateDecrypt({
- key: rsaPkcs8KeyPem,
- padding: padding,
- oaepHash: decryptOaepHash
- }, encryptedBuffer);
- assert.deepStrictEqual(decryptedBuffer, input);
+ if (padding === constants.RSA_PKCS1_PADDING) {
+ assert.throws(() => {
+ crypto.privateDecrypt({
+ key: rsaKeyPem,
+ padding: padding,
+ oaepHash: decryptOaepHash
+ }, encryptedBuffer);
+ }, { code: 'ERR_INVALID_ARG_VALUE' });
+ assert.throws(() => {
+ crypto.privateDecrypt({
+ key: rsaPkcs8KeyPem,
+ padding: padding,
+ oaepHash: decryptOaepHash
+ }, encryptedBuffer);
+ }, { code: 'ERR_INVALID_ARG_VALUE' });
+ } else {
+ let decryptedBuffer = crypto.privateDecrypt({
+ key: rsaKeyPem,
+ padding: padding,
+ oaepHash: decryptOaepHash
+ }, encryptedBuffer);
+ assert.deepStrictEqual(decryptedBuffer, input);
+
+ decryptedBuffer = crypto.privateDecrypt({
+ key: rsaPkcs8KeyPem,
+ padding: padding,
+ oaepHash: decryptOaepHash
+ }, encryptedBuffer);
+ assert.deepStrictEqual(decryptedBuffer, input);
+ }
}
test_rsa('RSA_NO_PADDING');

565
CVE-2024-22019.patch Normal file
View File

@ -0,0 +1,565 @@
From: Paolo Insogna <paolo@cowtech.it>
Date: Tue, 9 Jan 2024 18:10:04 +0100
Subject: CVE-2024-22019: http: add maximum chunk extension size
PR-URL: https://github.com/nodejs-private/node-private/pull/520
Refs: https://github.com/nodejs-private/node-private/pull/518
CVE-ID: CVE-2024-22019
origin: backport, https://github.com/nodejs/node/commit/911cb33cdadab57a75f97186290ea8f3903a6171.patch
bug: https://nodejs.org/en/blog/vulnerability/february-2024-security-releases/#reading-unprocessed-http-request-with-unbounded-chunk-extension-allows-dos-attacks-cve-2024-22019---high
---
deps/llhttp/include/llhttp.h | 4 +
deps/llhttp/src/api.c | 22 ++++
deps/llhttp/src/llhttp.c | 122 +++++++++++++++++---
doc/api/errors.md | 12 ++
lib/_http_server.js | 25 ++++-
src/node_http_parser_impl.h | 25 ++++-
test/parallel/test-http-chunk-extensions-limit.js | 131 ++++++++++++++++++++++
7 files changed, 324 insertions(+), 17 deletions(-)
create mode 100644 test/parallel/test-http-chunk-extensions-limit.js
diff --git a/deps/llhttp/include/llhttp.h b/deps/llhttp/include/llhttp.h
index fe3a927..d9cf6d3 100644
--- a/deps/llhttp/include/llhttp.h
+++ b/deps/llhttp/include/llhttp.h
@@ -255,6 +255,10 @@ struct llhttp_settings_s {
*/
llhttp_cb on_headers_complete;
+ /* Possible return values 0, -1, HPE_USER */
+ llhttp_data_cb on_chunk_parameters;
+
+ /* Possible return values 0, -1, HPE_USER */
llhttp_data_cb on_body;
/* Possible return values 0, -1, `HPE_PAUSED` */
diff --git a/deps/llhttp/src/api.c b/deps/llhttp/src/api.c
index 6f72465..1b7ad0e 100644
--- a/deps/llhttp/src/api.c
+++ b/deps/llhttp/src/api.c
@@ -15,6 +15,21 @@
err = settings->NAME(__VA_ARGS__); \
} while (0)
+#define SPAN_CALLBACK_MAYBE(PARSER, NAME, START, LEN) \
+ do { \
+ const llhttp_settings_t* settings; \
+ settings = (const llhttp_settings_t*) (PARSER)->settings; \
+ if (settings == NULL || settings->NAME == NULL) { \
+ err = 0; \
+ break; \
+ } \
+ err = settings->NAME((PARSER), (START), (LEN)); \
+ if (err == -1) { \
+ err = HPE_USER; \
+ llhttp_set_error_reason((PARSER), "Span callback error in " #NAME); \
+ } \
+ } while (0)
+
void llhttp_init(llhttp_t* parser, llhttp_type_t type,
const llhttp_settings_t* settings) {
llhttp__internal_init(parser);
@@ -202,6 +217,13 @@ int llhttp__on_chunk_header(llhttp_t* s, const char* p, const char* endp) {
}
+int llhttp__on_chunk_parameters(llhttp_t* s, const char* p, const char* endp) {
+ int err;
+ SPAN_CALLBACK_MAYBE(s, on_chunk_parameters, p, endp - p);
+ return err;
+}
+
+
int llhttp__on_chunk_complete(llhttp_t* s, const char* p, const char* endp) {
int err;
CALLBACK_MAYBE(s, on_chunk_complete, s);
diff --git a/deps/llhttp/src/llhttp.c b/deps/llhttp/src/llhttp.c
index e8f42ce..3fcea4b 100644
--- a/deps/llhttp/src/llhttp.c
+++ b/deps/llhttp/src/llhttp.c
@@ -307,6 +307,8 @@ enum llparse_state_e {
s_n_llhttp__internal__n_invoke_is_equal_content_length,
s_n_llhttp__internal__n_chunk_size_almost_done,
s_n_llhttp__internal__n_chunk_parameters,
+ s_n_llhttp__internal__n_span_start_llhttp__on_chunk_parameters,
+ s_n_llhttp__internal__n_chunk_parameters_ows,
s_n_llhttp__internal__n_chunk_size_otherwise,
s_n_llhttp__internal__n_chunk_size,
s_n_llhttp__internal__n_chunk_size_digit,
@@ -482,6 +484,10 @@ int llhttp__on_body(
llhttp__internal_t* s, const unsigned char* p,
const unsigned char* endp);
+int llhttp__on_chunk_parameters(
+ llhttp__internal_t* s, const unsigned char* p,
+ const unsigned char* endp);
+
int llhttp__on_status(
llhttp__internal_t* s, const unsigned char* p,
const unsigned char* endp);
@@ -1118,8 +1124,7 @@ static llparse_state_t llhttp__internal__run(
goto s_n_llhttp__internal__n_chunk_parameters;
}
case 2: {
- p++;
- goto s_n_llhttp__internal__n_chunk_size_almost_done;
+ goto s_n_llhttp__internal__n_span_end_llhttp__on_chunk_parameters;
}
default: {
goto s_n_llhttp__internal__n_error_10;
@@ -1128,6 +1133,34 @@ static llparse_state_t llhttp__internal__run(
/* UNREACHABLE */;
abort();
}
+ case s_n_llhttp__internal__n_span_start_llhttp__on_chunk_parameters:
+ s_n_llhttp__internal__n_span_start_llhttp__on_chunk_parameters: {
+ if (p == endp) {
+ return s_n_llhttp__internal__n_span_start_llhttp__on_chunk_parameters;
+ }
+ state->_span_pos0 = (void*) p;
+ state->_span_cb0 = llhttp__on_chunk_parameters;
+ goto s_n_llhttp__internal__n_chunk_parameters;
+ /* UNREACHABLE */;
+ abort();
+ }
+ case s_n_llhttp__internal__n_chunk_parameters_ows:
+ s_n_llhttp__internal__n_chunk_parameters_ows: {
+ if (p == endp) {
+ return s_n_llhttp__internal__n_chunk_parameters_ows;
+ }
+ switch (*p) {
+ case ' ': {
+ p++;
+ goto s_n_llhttp__internal__n_chunk_parameters_ows;
+ }
+ default: {
+ goto s_n_llhttp__internal__n_span_start_llhttp__on_chunk_parameters;
+ }
+ }
+ /* UNREACHABLE */;
+ abort();
+ }
case s_n_llhttp__internal__n_chunk_size_otherwise:
s_n_llhttp__internal__n_chunk_size_otherwise: {
if (p == endp) {
@@ -1138,13 +1171,9 @@ static llparse_state_t llhttp__internal__run(
p++;
goto s_n_llhttp__internal__n_chunk_size_almost_done;
}
- case ' ': {
- p++;
- goto s_n_llhttp__internal__n_chunk_parameters;
- }
case ';': {
p++;
- goto s_n_llhttp__internal__n_chunk_parameters;
+ goto s_n_llhttp__internal__n_chunk_parameters_ows;
}
default: {
goto s_n_llhttp__internal__n_error_11;
@@ -5449,6 +5478,24 @@ static llparse_state_t llhttp__internal__run(
/* UNREACHABLE */;
abort();
}
+ s_n_llhttp__internal__n_span_end_llhttp__on_chunk_parameters: {
+ const unsigned char* start;
+ int err;
+
+ start = state->_span_pos0;
+ state->_span_pos0 = NULL;
+ err = llhttp__on_chunk_parameters(state, start, p);
+ if (err != 0) {
+ state->error = err;
+ state->error_pos = (const char*) (p + 1);
+ state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_chunk_size_almost_done;
+ return s_error;
+ }
+ p++;
+ goto s_n_llhttp__internal__n_chunk_size_almost_done;
+ /* UNREACHABLE */;
+ abort();
+ }
s_n_llhttp__internal__n_error_10: {
state->error = 0x2;
state->reason = "Invalid character in chunk parameters";
@@ -7414,6 +7461,8 @@ enum llparse_state_e {
s_n_llhttp__internal__n_invoke_is_equal_content_length,
s_n_llhttp__internal__n_chunk_size_almost_done,
s_n_llhttp__internal__n_chunk_parameters,
+ s_n_llhttp__internal__n_span_start_llhttp__on_chunk_parameters,
+ s_n_llhttp__internal__n_chunk_parameters_ows,
s_n_llhttp__internal__n_chunk_size_otherwise,
s_n_llhttp__internal__n_chunk_size,
s_n_llhttp__internal__n_chunk_size_digit,
@@ -7584,6 +7633,10 @@ int llhttp__on_body(
llhttp__internal_t* s, const unsigned char* p,
const unsigned char* endp);
+int llhttp__on_chunk_parameters(
+ llhttp__internal_t* s, const unsigned char* p,
+ const unsigned char* endp);
+
int llhttp__on_status(
llhttp__internal_t* s, const unsigned char* p,
const unsigned char* endp);
@@ -8185,8 +8238,7 @@ static llparse_state_t llhttp__internal__run(
goto s_n_llhttp__internal__n_chunk_parameters;
}
case 2: {
- p++;
- goto s_n_llhttp__internal__n_chunk_size_almost_done;
+ goto s_n_llhttp__internal__n_span_end_llhttp__on_chunk_parameters;
}
default: {
goto s_n_llhttp__internal__n_error_6;
@@ -8195,6 +8247,34 @@ static llparse_state_t llhttp__internal__run(
/* UNREACHABLE */;
abort();
}
+ case s_n_llhttp__internal__n_span_start_llhttp__on_chunk_parameters:
+ s_n_llhttp__internal__n_span_start_llhttp__on_chunk_parameters: {
+ if (p == endp) {
+ return s_n_llhttp__internal__n_span_start_llhttp__on_chunk_parameters;
+ }
+ state->_span_pos0 = (void*) p;
+ state->_span_cb0 = llhttp__on_chunk_parameters;
+ goto s_n_llhttp__internal__n_chunk_parameters;
+ /* UNREACHABLE */;
+ abort();
+ }
+ case s_n_llhttp__internal__n_chunk_parameters_ows:
+ s_n_llhttp__internal__n_chunk_parameters_ows: {
+ if (p == endp) {
+ return s_n_llhttp__internal__n_chunk_parameters_ows;
+ }
+ switch (*p) {
+ case ' ': {
+ p++;
+ goto s_n_llhttp__internal__n_chunk_parameters_ows;
+ }
+ default: {
+ goto s_n_llhttp__internal__n_span_start_llhttp__on_chunk_parameters;
+ }
+ }
+ /* UNREACHABLE */;
+ abort();
+ }
case s_n_llhttp__internal__n_chunk_size_otherwise:
s_n_llhttp__internal__n_chunk_size_otherwise: {
if (p == endp) {
@@ -8205,13 +8285,9 @@ static llparse_state_t llhttp__internal__run(
p++;
goto s_n_llhttp__internal__n_chunk_size_almost_done;
}
- case ' ': {
- p++;
- goto s_n_llhttp__internal__n_chunk_parameters;
- }
case ';': {
p++;
- goto s_n_llhttp__internal__n_chunk_parameters;
+ goto s_n_llhttp__internal__n_chunk_parameters_ows;
}
default: {
goto s_n_llhttp__internal__n_error_7;
@@ -12312,6 +12388,24 @@ static llparse_state_t llhttp__internal__run(
/* UNREACHABLE */;
abort();
}
+ s_n_llhttp__internal__n_span_end_llhttp__on_chunk_parameters: {
+ const unsigned char* start;
+ int err;
+
+ start = state->_span_pos0;
+ state->_span_pos0 = NULL;
+ err = llhttp__on_chunk_parameters(state, start, p);
+ if (err != 0) {
+ state->error = err;
+ state->error_pos = (const char*) (p + 1);
+ state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_chunk_size_almost_done;
+ return s_error;
+ }
+ p++;
+ goto s_n_llhttp__internal__n_chunk_size_almost_done;
+ /* UNREACHABLE */;
+ abort();
+ }
s_n_llhttp__internal__n_error_6: {
state->error = 0x2;
state->reason = "Invalid character in chunk parameters";
diff --git a/doc/api/errors.md b/doc/api/errors.md
index 4dbb51b..3c8a858 100644
--- a/doc/api/errors.md
+++ b/doc/api/errors.md
@@ -2212,6 +2212,18 @@ malconfigured clients, if more than 8KB of HTTP header data is received then
HTTP parsing will abort without a request or response object being created, and
an `Error` with this code will be emitted.
+<a id="HPE_CHUNK_EXTENSIONS_OVERFLOW"></a>
+
+### `HPE_CHUNK_EXTENSIONS_OVERFLOW`
+
+<!-- YAML
+added: REPLACEME
+-->
+
+Too much data was received for a chunk extensions. In order to protect against
+malicious or malconfigured clients, if more than 16 KiB of data is received
+then an `Error` with this code will be emitted.
+
<a id="HPE_UNEXPECTED_CONTENT_LENGTH"></a>
### `HPE_UNEXPECTED_CONTENT_LENGTH`
diff --git a/lib/_http_server.js b/lib/_http_server.js
index 4d1f8e4..31ebae0 100644
--- a/lib/_http_server.js
+++ b/lib/_http_server.js
@@ -591,6 +591,11 @@ const requestHeaderFieldsTooLargeResponse = Buffer.from(
`HTTP/1.1 431 ${STATUS_CODES[431]}${CRLF}` +
`Connection: close${CRLF}${CRLF}`, 'ascii'
);
+const requestChunkExtensionsTooLargeResponse = Buffer.from(
+ `HTTP/1.1 413 ${STATUS_CODES[413]}\r\n` +
+ 'Connection: close\r\n\r\n', 'ascii',
+);
+
function socketOnError(e) {
// Ignore further errors
this.removeListener('error', socketOnError);
@@ -598,8 +603,24 @@ function socketOnError(e) {
if (!this.server.emit('clientError', e, this)) {
if (this.writable && this.bytesWritten === 0) {
- const response = e.code === 'HPE_HEADER_OVERFLOW' ?
- requestHeaderFieldsTooLargeResponse : badRequestResponse;
+ let response;
+
+ switch (e.code) {
+ case 'HPE_HEADER_OVERFLOW':
+ response = requestHeaderFieldsTooLargeResponse;
+ break;
+ case 'HPE_CHUNK_EXTENSIONS_OVERFLOW':
+ response = requestChunkExtensionsTooLargeResponse;
+ break;
+/* case 'ERR_HTTP_REQUEST_TIMEOUT':
+ response = requestTimeoutResponse;
+ break;
+*/
+ default:
+ response = badRequestResponse;
+ break;
+ }
+
this.write(response);
}
this.destroy(e);
diff --git a/src/node_http_parser_impl.h b/src/node_http_parser_impl.h
index 77d09a9..891a108 100644
--- a/src/node_http_parser_impl.h
+++ b/src/node_http_parser_impl.h
@@ -84,6 +84,8 @@ const uint32_t kOnExecute = 4;
const uint32_t kOnTimeout = 5;
// Any more fields than this will be flushed into JS
const size_t kMaxHeaderFieldsCount = 32;
+// Maximum size of chunk extensions
+const size_t kMaxChunkExtensionsSize = 16384;
inline bool IsOWS(char c) {
return c == ' ' || c == '\t';
@@ -187,6 +189,9 @@ class Parser : public AsyncWrap, public StreamListener {
int on_message_begin() {
num_fields_ = num_values_ = 0;
+#ifdef NODE_EXPERIMENTAL_HTTP
+ chunk_extensions_nread_ = 0;
+#endif
url_.Reset();
status_message_.Reset();
header_parsing_start_time_ = uv_hrtime();
@@ -432,9 +437,22 @@ class Parser : public AsyncWrap, public StreamListener {
}
#ifdef NODE_EXPERIMENTAL_HTTP
- // Reset nread for the next chunk
+ int on_chunk_extension(const char* at, size_t length) {
+ chunk_extensions_nread_ += length;
+
+ if (chunk_extensions_nread_ > kMaxChunkExtensionsSize) {
+ llhttp_set_error_reason(&parser_,
+ "HPE_CHUNK_EXTENSIONS_OVERFLOW:Chunk extensions overflow");
+ return HPE_USER;
+ }
+
+ return 0;
+ }
+
+ // Reset nread for the next chunk and also reset the extensions counter
int on_chunk_header() {
header_nread_ = 0;
+ chunk_extensions_nread_ = 0;
return 0;
}
@@ -857,6 +875,7 @@ class Parser : public AsyncWrap, public StreamListener {
llhttp_init(&parser_, type, &settings);
llhttp_set_lenient(&parser_, lenient);
header_nread_ = 0;
+ chunk_extensions_nread_ = 0;
#else /* !NODE_EXPERIMENTAL_HTTP */
http_parser_init(&parser_, type);
parser_.lenient_http_headers = lenient;
@@ -916,6 +935,7 @@ class Parser : public AsyncWrap, public StreamListener {
unsigned int execute_depth_ = 0;
bool pending_pause_ = false;
uint64_t header_nread_ = 0;
+ uint64_t chunk_extensions_nread_ = 0;
#endif /* NODE_EXPERIMENTAL_HTTP */
uint64_t headers_timeout_;
uint64_t header_parsing_start_time_ = 0;
@@ -948,6 +968,9 @@ const parser_settings_t Parser::settings = {
Proxy<DataCall, &Parser::on_header_field>::Raw,
Proxy<DataCall, &Parser::on_header_value>::Raw,
Proxy<Call, &Parser::on_headers_complete>::Raw,
+#ifdef NODE_EXPERIMENTAL_HTTP
+ Proxy<DataCall, &Parser::on_chunk_extension>::Raw,
+#endif
Proxy<DataCall, &Parser::on_body>::Raw,
Proxy<Call, &Parser::on_message_complete>::Raw,
#ifdef NODE_EXPERIMENTAL_HTTP
diff --git a/test/parallel/test-http-chunk-extensions-limit.js b/test/parallel/test-http-chunk-extensions-limit.js
new file mode 100644
index 0000000..6868b3d
--- /dev/null
+++ b/test/parallel/test-http-chunk-extensions-limit.js
@@ -0,0 +1,131 @@
+'use strict';
+
+const common = require('../common');
+const http = require('http');
+const net = require('net');
+const assert = require('assert');
+
+// Verify that chunk extensions are limited in size when sent all together.
+{
+ const server = http.createServer((req, res) => {
+ req.on('end', () => {
+ res.writeHead(200, { 'Content-Type': 'text/plain' });
+ res.end('bye');
+ });
+
+ req.resume();
+ });
+
+ server.listen(0, () => {
+ const sock = net.connect(server.address().port);
+ let data = '';
+
+ sock.on('data', (chunk) => data += chunk.toString('utf-8'));
+
+ sock.on('end', common.mustCall(function() {
+ assert.strictEqual(data, 'HTTP/1.1 413 Payload Too Large\r\nConnection: close\r\n\r\n');
+ server.close();
+ }));
+
+ sock.end('' +
+ 'GET / HTTP/1.1\r\n' +
+ 'Host: localhost:8080\r\n' +
+ 'Transfer-Encoding: chunked\r\n\r\n' +
+ '2;' + 'A'.repeat(20000) + '=bar\r\nAA\r\n' +
+ '0\r\n\r\n'
+ );
+ });
+}
+
+// Verify that chunk extensions are limited in size when sent in intervals.
+{
+ const server = http.createServer((req, res) => {
+ req.on('end', () => {
+ res.writeHead(200, { 'Content-Type': 'text/plain' });
+ res.end('bye');
+ });
+
+ req.resume();
+ });
+
+ server.listen(0, () => {
+ const sock = net.connect(server.address().port);
+ let remaining = 20000;
+ let data = '';
+
+ const interval = setInterval(
+ () => {
+ if (remaining > 0) {
+ sock.write('A'.repeat(1000));
+ } else {
+ sock.write('=bar\r\nAA\r\n0\r\n\r\n');
+ clearInterval(interval);
+ }
+
+ remaining -= 1000;
+ },
+ common.platformTimeout(20),
+ ).unref();
+
+ sock.on('data', (chunk) => data += chunk.toString('utf-8'));
+
+ sock.on('end', common.mustCall(function() {
+ assert.strictEqual(data, 'HTTP/1.1 413 Payload Too Large\r\nConnection: close\r\n\r\n');
+ server.close();
+ }));
+
+ sock.write('' +
+ 'GET / HTTP/1.1\r\n' +
+ 'Host: localhost:8080\r\n' +
+ 'Transfer-Encoding: chunked\r\n\r\n' +
+ '2;'
+ );
+ });
+}
+
+// Verify the chunk extensions is correctly reset after a chunk
+{
+ const server = http.createServer((req, res) => {
+ req.on('end', () => {
+ res.writeHead(200, { 'content-type': 'text/plain', 'connection': 'close', 'date': 'now' });
+ res.end('bye');
+ });
+
+ req.resume();
+ });
+
+ server.listen(0, () => {
+ const sock = net.connect(server.address().port);
+ let data = '';
+
+ sock.on('data', (chunk) => data += chunk.toString('utf-8'));
+
+ sock.on('end', common.mustCall(function() {
+ assert.strictEqual(
+ data,
+ 'HTTP/1.1 200 OK\r\n' +
+ 'content-type: text/plain\r\n' +
+ 'connection: close\r\n' +
+ 'date: now\r\n' +
+ 'Transfer-Encoding: chunked\r\n' +
+ '\r\n' +
+ '3\r\n' +
+ 'bye\r\n' +
+ '0\r\n' +
+ '\r\n',
+ );
+
+ server.close();
+ }));
+
+ sock.end('' +
+ 'GET / HTTP/1.1\r\n' +
+ 'Host: localhost:8080\r\n' +
+ 'Transfer-Encoding: chunked\r\n\r\n' +
+ '2;' + 'A'.repeat(10000) + '=bar\r\nAA\r\n' +
+ '2;' + 'A'.repeat(10000) + '=bar\r\nAA\r\n' +
+ '2;' + 'A'.repeat(10000) + '=bar\r\nAA\r\n' +
+ '0\r\n\r\n'
+ );
+ });
+}

154
CVE-2024-22025.patch Normal file

File diff suppressed because one or more lines are too long

3031
CVE-2024-27982.patch Normal file

File diff suppressed because it is too large Load Diff

34
CVE-2024-27983.patch Normal file
View File

@ -0,0 +1,34 @@
From: RafaelGSS <rafael.nunu@hotmail.com>
Date: Tue, 26 Mar 2024 15:55:13 -0300
Subject: CVE-2024-27983 ensure to close stream when destroying session
Co-Authored-By: Anna Henningsen <anna@addaleax.net>
PR-URL: https://github.com/nodejs-private/node-private/pull/561
bug-hakerone: https://hackerone.com/reports/2319584
Reviewed-By: Michael Dawson <midawson@redhat.com>
Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
CVE-ID: CVE-2024-27983
origin: backport, https://github.com/nodejs/node/commit/0fb816dbccde955cd24acc1b16497a91fab507c8.patch
---
src/node_http2.cc | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/src/node_http2.cc b/src/node_http2.cc
index 5156aa3..c441921 100644
--- a/src/node_http2.cc
+++ b/src/node_http2.cc
@@ -590,6 +590,12 @@ Http2Session::Http2Session(Environment* env,
Http2Session::~Http2Session() {
CHECK_EQ(flags_ & SESSION_STATE_HAS_SCOPE, 0);
Debug(this, "freeing nghttp2 session");
+ // Ensure that all `Http2Stream` instances and the memory they hold
+ // on to are destroyed before the nghttp2 session is.
+ for (const auto& [id, stream] : streams_) {
+ stream->Detach();
+ }
+ streams_.clear();
nghttp2_session_del(session_);
CHECK_EQ(current_nghttp2_memory_, 0);
}

364
CVE-2025-23085.patch Normal file
View File

@ -0,0 +1,364 @@
From: RafaelGSS <rafael.nunu@hotmail.com>
Date: Tue, 17 Dec 2024 16:58:03 -0300
Subject: [PATCH] src: fix HTTP2 mem leak on premature close and ERR_PROTO
This commit fixes a memory leak when the socket is
suddenly closed by the peer (without GOAWAY notification)
and when invalid header (by nghttp2) is identified and the
connection is terminated by peer.
[backport]
- test remove assert.strictEqual(session instanceof ServerHttp2Session, true); that fail on old version without ponyfill
- force non specific error in order to avoid an assert
Refs: https://hackerone.com/reports/2841362
PR-URL: https://github.com/nodejs-private/node-private/pull/650
Reviewed-By: James M Snell <jasnell@gmail.com>
CVE-ID: CVE-2025-23085
origin: backport, https://github.com/nodejs/node/commit/6cc8d58e6f97c37c228f134bd9b98246c8871fb1
bug: https://nodejs.org/en/blog/vulnerability/january-2025-security-releases#goaway-http2-frames-cause-memory-leak-outside-heap-cve-2025-23085---medium
---
lib/internal/http2/core.js | 14 +++-
src/node_http2.cc | 37 +++++++--
...-http2-connect-method-extended-cant-turn-off.js | 6 ++
test/parallel/test-http2-invalid-last-stream-id.js | 76 +++++++++++++++++++
.../test-http2-options-max-headers-block-length.js | 2 +
test/parallel/test-http2-premature-close.js | 88 ++++++++++++++++++++++
6 files changed, 216 insertions(+), 7 deletions(-)
create mode 100644 test/parallel/test-http2-invalid-last-stream-id.js
create mode 100644 test/parallel/test-http2-premature-close.js
diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js
index 1c6767c..d7caae3 100644
--- a/lib/internal/http2/core.js
+++ b/lib/internal/http2/core.js
@@ -573,9 +573,21 @@ function onFrameError(id, type, code) {
return;
debugSessionObj(session, 'error sending frame type %d on stream %d, code: %d',
type, id, code);
- const emitter = session[kState].streams.get(id) || session;
+
+ const stream = session[kState].streams.get(id);
+ const emitter = stream || session;
emitter[kUpdateTimer]();
emitter.emit('frameError', type, code, id);
+
+ // When a frameError happens is not uncommon that a pending GOAWAY
+ // package from nghttp2 is on flight with a correct error code.
+ // We schedule it using setImmediate to give some time for that
+ // package to arrive.
+ setImmediate(() => {
+ if(stream)
+ stream.close(NGHTTP2_INTERNAL_ERROR);
+ session.close();
+ });
}
function onAltSvc(stream, origin, alt) {
diff --git a/src/node_http2.cc b/src/node_http2.cc
index c441921..6365734 100644
--- a/src/node_http2.cc
+++ b/src/node_http2.cc
@@ -742,6 +742,7 @@ inline bool Http2Session::CanAddStream() {
}
inline void Http2Session::AddStream(Http2Stream* stream) {
+ Debug(this, "Adding stream: %d", stream->id());
CHECK_GE(++statistics_.stream_count, 0);
streams_[stream->id()] = stream;
size_t size = streams_.size();
@@ -750,10 +751,10 @@ inline void Http2Session::AddStream(Http2Stream* stream) {
IncrementCurrentSessionMemory(sizeof(*stream));
}
-
inline void Http2Session::RemoveStream(Http2Stream* stream) {
if (streams_.empty() || stream == nullptr)
return; // Nothing to remove, item was never added?
+ Debug(this, "Removing stream: %d", stream->id());
streams_.erase(stream->id());
DecrementCurrentSessionMemory(sizeof(*stream));
}
@@ -926,6 +927,7 @@ int Http2Session::OnHeaderCallback(nghttp2_session* handle,
if (UNLIKELY(stream == nullptr))
return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE;
+ Debug(session, "handling header key/pair for stream %d", id);
// If the stream has already been destroyed, ignore.
if (!stream->IsDestroyed() && !stream->AddHeader(name, value, flags)) {
// This will only happen if the connected peer sends us more
@@ -995,9 +997,21 @@ int Http2Session::OnInvalidFrame(nghttp2_session* handle,
return 1;
}
- // If the error is fatal or if error code is ERR_STREAM_CLOSED... emit error
+ // If the error is fatal or if error code is one of the following
+ // we emit and error:
+ //
+ // ERR_STREAM_CLOSED: An invalid frame has been received in a closed stream.
+ //
+ // ERR_PROTO: The RFC 7540 specifies:
+ // "An endpoint that encounters a connection error SHOULD first send a GOAWAY
+ // frame (Section 6.8) with the stream identifier of the last stream that it
+ // successfully received from its peer.
+ // The GOAWAY frame includes an error code that indicates the type of error"
+ // The GOAWAY frame is already sent by nghttp2. We emit the error
+ // to liberate the Http2Session to destroy.
if (nghttp2_is_fatal(lib_error_code) ||
- lib_error_code == NGHTTP2_ERR_STREAM_CLOSED) {
+ lib_error_code == NGHTTP2_ERR_STREAM_CLOSED ||
+ lib_error_code == NGHTTP2_ERR_PROTO) {
Environment* env = session->env();
Isolate* isolate = env->isolate();
HandleScope scope(isolate);
@@ -1024,12 +1038,19 @@ int Http2Session::OnFrameNotSent(nghttp2_session* handle,
Debug(session, "frame type %d was not sent, code: %d",
frame->hd.type, error_code);
- // Do not report if the frame was not sent due to the session closing
if (error_code == NGHTTP2_ERR_SESSION_CLOSING ||
error_code == NGHTTP2_ERR_STREAM_CLOSED ||
error_code == NGHTTP2_ERR_STREAM_CLOSING ||
session->js_fields_.frame_error_listener_count == 0) {
- return 0;
+ // Currently, nghttp2 doesn't not inform us when is the best
+ // time to call session.close(). It relies on a closing connection
+ // from peer. If that doesn't happen, the nghttp2_session will be
+ // closed but the Http2Session will still be up causing a memory leak.
+ // Therefore, if the GOAWAY frame couldn't be send due to
+ // ERR_SESSION_CLOSING we should force close from our side.
+ if (frame->hd.type != 0x03) {
+ return 0;
+ }
}
Isolate* isolate = env->isolate();
@@ -1095,12 +1116,15 @@ int Http2Session::OnStreamClose(nghttp2_session* handle,
// ignore these. If this callback was not provided, nghttp2 would handle
// invalid headers strictly and would shut down the stream. We are intentionally
// being more lenient here although we may want to revisit this choice later.
-int Http2Session::OnInvalidHeader(nghttp2_session* session,
+int Http2Session::OnInvalidHeader(nghttp2_session* handle,
const nghttp2_frame* frame,
nghttp2_rcbuf* name,
nghttp2_rcbuf* value,
uint8_t flags,
void* user_data) {
+ Http2Session* session = static_cast<Http2Session*>(user_data);
+ int32_t id = GetFrameID(frame);
+ Debug(session, "invalid header received for stream %d", id);
// Ignore invalid header fields by default.
return 0;
}
@@ -1494,6 +1518,7 @@ void Http2Session::HandlePingFrame(const nghttp2_frame* frame) {
// Called by OnFrameReceived when a complete SETTINGS frame has been received.
void Http2Session::HandleSettingsFrame(const nghttp2_frame* frame) {
+ Debug(this, "handling settings frame");
bool ack = frame->hd.flags & NGHTTP2_FLAG_ACK;
if (!ack) {
js_fields_.bitfield &= ~(1 << kSessionRemoteSettingsIsUpToDate);
diff --git a/test/parallel/test-http2-connect-method-extended-cant-turn-off.js b/test/parallel/test-http2-connect-method-extended-cant-turn-off.js
index f4d033e..456aa1c 100644
--- a/test/parallel/test-http2-connect-method-extended-cant-turn-off.js
+++ b/test/parallel/test-http2-connect-method-extended-cant-turn-off.js
@@ -27,4 +27,10 @@ server.listen(0, common.mustCall(() => {
server.close();
}));
}));
+
+ client.on('error', common.expectsError({
+ code: 'ERR_HTTP2_ERROR',
+ name: 'Error',
+ message: 'Protocol error'
+ }));
}));
diff --git a/test/parallel/test-http2-invalid-last-stream-id.js b/test/parallel/test-http2-invalid-last-stream-id.js
new file mode 100644
index 0000000..8e7beae
--- /dev/null
+++ b/test/parallel/test-http2-invalid-last-stream-id.js
@@ -0,0 +1,76 @@
+// Flags: --expose-internals
+'use strict';
+
+const common = require('../common');
+if (!common.hasCrypto) common.skip('missing crypto');
+
+const h2 = require('http2');
+const net = require('net');
+const assert = require('assert');
+const { ServerHttp2Session } = require('internal/http2/core');
+
+async function sendInvalidLastStreamId(server) {
+ const client = new net.Socket();
+
+ const address = server.address();
+ if (!common.hasIPv6 && address.family === 'IPv6') {
+ // Necessary to pass CI running inside containers.
+ client.connect(address.port);
+ } else {
+ client.connect(address);
+ }
+
+ client.on('connect', common.mustCall(function() {
+ // HTTP/2 preface
+ client.write(Buffer.from('PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n', 'utf8'));
+
+ // Empty SETTINGS frame
+ client.write(Buffer.from([0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00]));
+
+ // GOAWAY frame with custom debug message
+ const goAwayFrame = [
+ 0x00, 0x00, 0x21, // Length: 33 bytes
+ 0x07, // Type: GOAWAY
+ 0x00, // Flags
+ 0x00, 0x00, 0x00, 0x00, // Stream ID: 0
+ 0x00, 0x00, 0x00, 0x01, // Last Stream ID: 1
+ 0x00, 0x00, 0x00, 0x00, // Error Code: 0 (No error)
+ ];
+
+ // Add the debug message
+ const debugMessage = 'client transport shutdown';
+ const goAwayBuffer = Buffer.concat([
+ Buffer.from(goAwayFrame),
+ Buffer.from(debugMessage, 'utf8'),
+ ]);
+
+ client.write(goAwayBuffer);
+ client.destroy();
+ }));
+}
+
+const server = h2.createServer();
+
+server.on('error', common.mustNotCall());
+
+server.on(
+ 'sessionError',
+ common.mustCall((err, session) => {
+ // When destroying the session, on Windows, we would get ECONNRESET
+ // errors, make sure we take those into account in our tests.
+ if (err.code !== 'ECONNRESET') {
+ assert.strictEqual(err.code, 'ERR_HTTP2_ERROR');
+ assert.strictEqual(err.name, 'Error');
+ assert.strictEqual(err.message, 'Protocol error');
+ }
+ session.close();
+ server.close();
+ }),
+);
+
+server.listen(
+ 0,
+ common.mustCall(async () => {
+ await sendInvalidLastStreamId(server);
+ }),
+);
diff --git a/test/parallel/test-http2-options-max-headers-block-length.js b/test/parallel/test-http2-options-max-headers-block-length.js
index 11632c6..a74e1e1 100644
--- a/test/parallel/test-http2-options-max-headers-block-length.js
+++ b/test/parallel/test-http2-options-max-headers-block-length.js
@@ -35,6 +35,8 @@ server.listen(0, common.mustCall(() => {
assert.strictEqual(code, h2.constants.NGHTTP2_ERR_FRAME_SIZE_ERROR);
}));
+ // NGHTTP2 will automatically send the NGHTTP2_REFUSED_STREAM with
+ // the GOAWAY frame.
req.on('error', common.expectsError({
code: 'ERR_HTTP2_STREAM_ERROR',
name: 'Error',
diff --git a/test/parallel/test-http2-premature-close.js b/test/parallel/test-http2-premature-close.js
new file mode 100644
index 0000000..a9b08f5
--- /dev/null
+++ b/test/parallel/test-http2-premature-close.js
@@ -0,0 +1,88 @@
+// Flags: --expose-internals
+'use strict';
+
+const common = require('../common');
+if (!common.hasCrypto) common.skip('missing crypto');
+
+const h2 = require('http2');
+const net = require('net');
+
+async function requestAndClose(server) {
+ const client = new net.Socket();
+
+ const address = server.address();
+ if (!common.hasIPv6 && address.family === 'IPv6') {
+ // Necessary to pass CI running inside containers.
+ client.connect(address.port);
+ } else {
+ client.connect(address);
+ }
+
+ client.on('connect', common.mustCall(function() {
+ // Send HTTP/2 Preface
+ client.write(Buffer.from('PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n', 'utf8'));
+
+ // Send a SETTINGS frame (empty payload)
+ client.write(Buffer.from([0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00]));
+
+ const streamId = 1;
+ // Send a valid HEADERS frame
+ const headersFrame = Buffer.concat([
+ Buffer.from([
+ 0x00, 0x00, 0x0c, // Length: 12 bytes
+ 0x01, // Type: HEADERS
+ 0x05, // Flags: END_HEADERS + END_STREAM
+ (streamId >> 24) & 0xFF, // Stream ID: high byte
+ (streamId >> 16) & 0xFF,
+ (streamId >> 8) & 0xFF,
+ streamId & 0xFF, // Stream ID: low byte
+ ]),
+ Buffer.from([
+ 0x82, // Indexed Header Field Representation (Predefined ":method: GET")
+ 0x84, // Indexed Header Field Representation (Predefined ":path: /")
+ 0x86, // Indexed Header Field Representation (Predefined ":scheme: http")
+ 0x44, 0x0a, // Custom ":authority: localhost"
+ 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x68, 0x6f, 0x73, 0x74,
+ ]),
+ ]);
+ client.write(headersFrame);
+
+ // Send a valid DATA frame
+ const dataFrame = Buffer.concat([
+ Buffer.from([
+ 0x00, 0x00, 0x05, // Length: 5 bytes
+ 0x00, // Type: DATA
+ 0x00, // Flags: No flags
+ (streamId >> 24) & 0xFF, // Stream ID: high byte
+ (streamId >> 16) & 0xFF,
+ (streamId >> 8) & 0xFF,
+ streamId & 0xFF, // Stream ID: low byte
+ ]),
+ Buffer.from('Hello', 'utf8'), // Data payload
+ ]);
+ client.write(dataFrame);
+
+ // Does not wait for server to reply. Shutdown the socket
+ client.end();
+ }));
+}
+
+const server = h2.createServer();
+
+server.on('error', common.mustNotCall());
+
+server.on(
+ 'session',
+ common.mustCall((session) => {
+ session.on('close', common.mustCall(() => {
+ server.close();
+ }));
+ }),
+);
+
+server.listen(
+ 0,
+ common.mustCall(async () => {
+ await requestAndClose(server);
+ }),
+);

View File

@ -1,5 +1,5 @@
%bcond_with bootstrap
%global baserelease 6
%global baserelease 12
%{?!_pkgdocdir:%global _pkgdocdir %{_docdir}/%{name}-%{version}}
%global nodejs_epoch 1
%global nodejs_major 12
@ -99,6 +99,20 @@ Patch00023: CVE-2022-25881.patch
Patch00024: CVE-2023-23920.patch
Patch00025: CVE-2023-32559.patch
Patch00026: CVE-2023-32002-CVE-2023-32006.patch
Patch00027: 0006-add-loong64-support-12.22.11.patch
# https://github.com/openssl/openssl/commit/879f7080d7e141f415c79eaa3a8ac4a3dad0348b
Patch00028: CVE-2023-0464.patch
# https://github.com/openssl/openssl/commit/b013765abfa80036dc779dd0e50602c57bb3bf95
Patch00029: CVE-2023-0465.patch
# https://github.com/nghttp2/nghttp2/commit/72b4af6143681f528f1d237b21a9a7aee1738832
Patch00030: CVE-2023-44487.patch
Patch00031: CVE-2023-46809.patch
Patch00032: CVE-2024-22019.patch
Patch00033: CVE-2024-22025.patch
Patch00034: CVE-2024-27982.patch
Patch00035: CVE-2024-27983.patch
Patch00036: CVE-2025-23085.patch
Patch00037: 0007-correct-some-errors-related-to-CVE-2025-23085.patch
BuildRequires: python3-devel
BuildRequires: zlib-devel
@ -501,6 +515,24 @@ end
%{_pkgdocdir}/npm/docs
%changelog
* Fri Apr 11 2025 hanguanqiang <hanguanqiang@kylinos.cn> - 1:12.22.11-12
- correct error related to CVE-2025-23085
* Wed Mar 05 2025 yaoxin <1024769339@qq.com> - 1:12.22.11-11
- Fix CVE-2025-23085
* Thu Sep 19 2024 yaoxin <yao_xin001@hoperun.com>- 1:12.22.11-10
- Fix CVE-2023-46809,CVE-2024-22019,CVE-2024-22025,CVE-2024-27982 and CVE-2024-27983
* Tue Feb 06 2024 yaoxin <yao_xin001@hoperun.com>- 1:12.22.11-9
- Fix CVE-2023-44487
* Mon Feb 05 2024 yaoxin <yao_xin001@hoperun.com>- 1:12.22.11-8
- Fix CVE-2023-0464 and CVE-2023-0465
* Mon Feb 05 2024 yaoxin <yao_xin001@hoperun.com>- 1:12.22.11-7
- add loong64 support
* Thu Oct 26 2023 wangkai <13474090681@163.com> - 1:12.22.11-6
- Update CVE-2023-23918.patch for fix nodejs-raw-body,nodejs-istanbul build error