Skip to content

Patch nodejs18 for CVE-2025-22150, CVE-2025-23085, CVE-2024-22020, CVE-2024-22195 [Medium] #12231

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Feb 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions SPECS/nodejs/CVE-2024-22020.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
From b2e718b4ce54713cd57207125cb02ff567a9bf04 Mon Sep 17 00:00:00 2001
From: Kanishk-Bansal <[email protected]>
Date: Tue, 11 Feb 2025 17:08:22 +0000
Subject: [PATCH] Fix CVE-2024-22020

---
lib/internal/modules/esm/resolve.js | 17 +++++++++++++++--
1 file changed, 15 insertions(+), 2 deletions(-)

diff --git a/lib/internal/modules/esm/resolve.js b/lib/internal/modules/esm/resolve.js
index 96dd20a8..de53b11f 100644
--- a/lib/internal/modules/esm/resolve.js
+++ b/lib/internal/modules/esm/resolve.js
@@ -1142,9 +1142,19 @@ function defaultResolve(specifier, context = {}) {
} else {
parsed = new URL(specifier);
}
-
// Avoid accessing the `protocol` property due to the lazy getters.
const protocol = parsed.protocol;
+
+ if (protocol === 'data:' &&
+ parsedParentURL.protocol !== 'file:' &&
+ experimentalNetworkImports) {
+ throw new ERR_NETWORK_IMPORT_DISALLOWED(
+ specifier,
+ parsedParentURL,
+ 'import data: from a non file: is not allowed',
+ );
+ }
+
if (protocol === 'data:' ||
(experimentalNetworkImports &&
(
@@ -1155,7 +1165,10 @@ function defaultResolve(specifier, context = {}) {
) {
return { __proto__: null, url: parsed.href };
}
- } catch {
+ } catch (e) {
+ if (e?.code === 'ERR_NETWORK_IMPORT_DISALLOWED') {
+ throw e;
+ }
// Ignore exception
}

--
2.45.2

60 changes: 60 additions & 0 deletions SPECS/nodejs/CVE-2024-22195.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
From 5bab005747120011816817aaf2174ab46d0c72c9 Mon Sep 17 00:00:00 2001
From: Kanishk-Bansal <[email protected]>
Date: Wed, 12 Feb 2025 10:20:59 +0000
Subject: [PATCH] Fix CVE-2024-22195
Backport of https://github.com/pallets/jinja/commit/7dd3680e6eea0d77fde024763657aa4d884ddb23

---
deps/v8/third_party/jinja2/filters.py | 24 +++++++++++++++++-------
1 file changed, 17 insertions(+), 7 deletions(-)

diff --git a/deps/v8/third_party/jinja2/filters.py b/deps/v8/third_party/jinja2/filters.py
index 74b108dc..46347251 100644
--- a/deps/v8/third_party/jinja2/filters.py
+++ b/deps/v8/third_party/jinja2/filters.py
@@ -204,12 +204,15 @@ def do_lower(s):
"""Convert a value to lowercase."""
return soft_unicode(s).lower()

+_space_re = re.compile(r"\s", flags=re.ASCII)
+

@evalcontextfilter
def do_xmlattr(_eval_ctx, d, autospace=True):
"""Create an SGML/XML attribute string based on the items in a dict.
- All values that are neither `none` nor `undefined` are automatically
- escaped:
+
+ If any key contains a space, this fails with a ``ValueError``. Values that
+ are neither ``none`` nor ``undefined`` are automatically escaped.

.. sourcecode:: html+jinja

@@ -228,12 +231,19 @@ def do_xmlattr(_eval_ctx, d, autospace=True):

As you can see it automatically prepends a space in front of the item
if the filter returned something unless the second parameter is false.
+
+ .. versionchanged:: 3.1.3
+ Keys with spaces are not allowed.
"""
- rv = u" ".join(
- u'%s="%s"' % (escape(key), escape(value))
- for key, value in iteritems(d)
- if value is not None and not isinstance(value, Undefined)
- )
+ items = []
+ for key, value in d.items():
+ if value is None or isinstance(value, Undefined):
+ continue
+ if _space_re.search(key) is not None:
+ raise ValueError(f"Spaces are not allowed in attributes: '{key}'")
+ items.append(f'{escape(key)}="{escape(value)}"')
+ rv = " ".join(items)
+
if autospace and rv:
rv = u" " + rv
if _eval_ctx.autoescape:
--
2.45.2

40 changes: 40 additions & 0 deletions SPECS/nodejs/CVE-2025-22150.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
From d059d899b9c92a1479432016fe48a01d9254d89a Mon Sep 17 00:00:00 2001
From: Kanishk Bansal <[email protected]>
Date: Wed, 5 Feb 2025 12:28:55 +0000
Subject: [PATCH] Address CVE-2025-22150

---
deps/undici/src/lib/fetch/body.js | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/deps/undici/src/lib/fetch/body.js b/deps/undici/src/lib/fetch/body.js
index fd8481b7..4afcfdfb 100644
--- a/deps/undici/src/lib/fetch/body.js
+++ b/deps/undici/src/lib/fetch/body.js
@@ -22,6 +22,14 @@ const { isUint8Array, isArrayBuffer } = require('util/types')
const { File: UndiciFile } = require('./file')
const { parseMIMEType, serializeAMimeType } = require('./dataURL')

+let random
+try {
+ const crypto = require('node:crypto')
+ random = (max) => crypto.randomInt(0, max)
+} catch {
+ random = (max) => Math.floor(Math.random(max))
+}
+
let ReadableStream = globalThis.ReadableStream

/** @type {globalThis['File']} */
@@ -107,7 +115,7 @@ function extractBody (object, keepalive = false) {
// Set source to a copy of the bytes held by object.
source = new Uint8Array(object.buffer.slice(object.byteOffset, object.byteOffset + object.byteLength))
} else if (util.isFormDataLike(object)) {
- const boundary = `----formdata-undici-0${`${Math.floor(Math.random() * 1e11)}`.padStart(11, '0')}`
+ const boundary = `----formdata-undici-0${`${random(1e11)}`.padStart(11, '0')}`
const prefix = `--${boundary}\r\nContent-Disposition: form-data`

/*! formdata-polyfill. MIT License. Jimmy Wärting <https://jimmy.warting.se/opensource> */
--
2.43.0

200 changes: 200 additions & 0 deletions SPECS/nodejs/CVE-2025-23085.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
From 4f5c7b360f31f31ac160b171861c5f52f48e367e Mon Sep 17 00:00:00 2001
From: Kanishk-Bansal <[email protected]>
Date: Tue, 11 Feb 2025 15:53:59 +0000
Subject: [PATCH] Fix CVE-2025-23085

---
lib/internal/http2/core.js | 15 ++++++--
src/node_http2.cc | 36 ++++++++++++++++---
...2-connect-method-extended-cant-turn-off.js | 6 ++++
...-http2-options-max-headers-block-length.js | 4 ++-
...tp2-options-max-headers-exceeds-nghttp2.js | 4 ++-
5 files changed, 55 insertions(+), 10 deletions(-)

diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js
index 92ce193b..38844d30 100644
--- a/lib/internal/http2/core.js
+++ b/lib/internal/http2/core.js
@@ -608,11 +608,20 @@ 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);
- session[kState].streams.get(id).close(code);
- session.close();
+
+ // 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(() => {
+ stream?.close(code);
+ session.close();
+ });
}

function onAltSvc(stream, origin, alt) {
diff --git a/src/node_http2.cc b/src/node_http2.cc
index eb3506ff..38d47f0c 100644
--- a/src/node_http2.cc
+++ b/src/node_http2.cc
@@ -750,6 +750,7 @@ bool Http2Session::CanAddStream() {
}

void Http2Session::AddStream(Http2Stream* stream) {
+ Debug(this, "Adding stream: %d", stream->id());
CHECK_GE(++statistics_.stream_count, 0);
streams_[stream->id()] = BaseObjectPtr<Http2Stream>(stream);
size_t size = streams_.size();
@@ -760,6 +761,7 @@ void Http2Session::AddStream(Http2Stream* stream) {


BaseObjectPtr<Http2Stream> Http2Session::RemoveStream(int32_t id) {
+ Debug(this, "Removing stream: %d", id);
BaseObjectPtr<Http2Stream> stream;
if (streams_.empty())
return stream;
@@ -936,6 +938,7 @@ int Http2Session::OnHeaderCallback(nghttp2_session* handle,
if (UNLIKELY(!stream))
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->is_destroyed() && !stream->AddHeader(name, value, flags)) {
// This will only happen if the connected peer sends us more
@@ -1005,9 +1008,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);
@@ -1070,7 +1085,6 @@ 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) {
@@ -1079,7 +1093,15 @@ int Http2Session::OnFrameNotSent(nghttp2_session* handle,
// to destroy the session completely.
// Further information see: https://github.com/nodejs/node/issues/35233
session->DecrefHeaders(frame);
- 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();
@@ -1145,12 +1167,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;
}
@@ -1544,6 +1569,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 f4d033ef..456aa1ce 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-options-max-headers-block-length.js b/test/parallel/test-http2-options-max-headers-block-length.js
index af1cc6f9..15b142ac 100644
--- a/test/parallel/test-http2-options-max-headers-block-length.js
+++ b/test/parallel/test-http2-options-max-headers-block-length.js
@@ -35,9 +35,11 @@ server.listen(0, common.mustCall(() => {
assert.strictEqual(code, h2.constants.NGHTTP2_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',
- message: 'Stream closed with error code NGHTTP2_FRAME_SIZE_ERROR'
+ message: 'Stream closed with error code NGHTTP2_REFUSED_STREAM'
}));
}));
diff --git a/test/parallel/test-http2-options-max-headers-exceeds-nghttp2.js b/test/parallel/test-http2-options-max-headers-exceeds-nghttp2.js
index df3aefff..7767dbbc 100644
--- a/test/parallel/test-http2-options-max-headers-exceeds-nghttp2.js
+++ b/test/parallel/test-http2-options-max-headers-exceeds-nghttp2.js
@@ -59,6 +59,9 @@ server.listen(0, common.mustCall(() => {
'session',
common.mustCall((session) => {
assert.strictEqual(session instanceof ServerHttp2Session, true);
+ session.on('close', common.mustCall(() => {
+ server.close();
+ }));
}),
);
server.on(
@@ -80,7 +83,6 @@ server.listen(0, common.mustCall(() => {
assert.strictEqual(err.name, 'Error');
assert.strictEqual(err.message, 'Session closed with error code 9');
assert.strictEqual(session instanceof ServerHttp2Session, true);
- server.close();
}),
);

--
2.45.2

9 changes: 8 additions & 1 deletion SPECS/nodejs/nodejs18.spec
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Name: nodejs18
# WARNINGS: MUST check and update the 'npm_version' macro for every version update of this package.
# The version of NPM can be found inside the sources under 'deps/npm/package.json'.
Version: 18.20.3
Release: 2%{?dist}
Release: 3%{?dist}
License: BSD and MIT and Public Domain and NAIST-2003 and Artistic-2.0
Group: Applications/System
Vendor: Microsoft Corporation
Expand All @@ -18,6 +18,10 @@ URL: https://github.com/nodejs/node
Source0: https://nodejs.org/download/release/v%{version}/node-v%{version}.tar.xz
Patch0: CVE-2023-21100.patch
Patch1: CVE-2024-21538.patch
Patch2: CVE-2025-22150.patch
Patch3: CVE-2025-23085.patch
Patch4: CVE-2024-22020.patch
Patch5: CVE-2024-22195.patch
BuildRequires: brotli-devel
BuildRequires: coreutils >= 8.22
BuildRequires: gcc
Expand Down Expand Up @@ -118,6 +122,9 @@ make cctest
%{_datadir}/systemtap/tapset/node.stp

%changelog
* Tue Feb 11 2025 Kanishk Bansal <[email protected]> - 18.20.3-3
- Patch CVE-2025-22150, CVE-2025-23085, CVE-2024-22020, CVE-2024-22195

* Tue Nov 19 2024 Bala <[email protected]> - 18.20.3-2
- Patch CVE-2024-21538

Expand Down
Loading