CVE-2024-38372 (published 2024-07-08T21:15Z)

Undici is an HTTP/1.1 client, written from scratch for Node.js. Depending on network and process conditions of a `fetch()` request, `response.arrayBuffer()` might include portion of memory from the Node.js process. This has been patched in v6.19.2.


Weakness information:
cwe_id: NVD-CWE-noinfo
cwe_name: Insufficient Information
description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
extended_description: Insufficient Information

Fix information:
repo_url: https://github.com/nodejs/undici
commit_details: repo_url: https://github.com/nodejs/undici
author: tsctx
author_date: 2024-04-16 02:54:28+09:00
msg: perf: avoid unnecessary clone (#3117)

* perf(fetch): improve body mixin methods

* perf: avoid unnecessary clone
repository: repo_url: https://github.com/nodejs/undici
repo_name: nodejs/undici
description: An HTTP/1.1 client, written from scratch for Node.js
repo_language: JavaScript
owner: nodejs
file_changes: filename: body-arraybuffer.mjs
change_type: ModificationType.ADD
diff: @@ -0,0 +1,24 @@
+import { group, bench, run } from 'mitata'
+import { Response } from '../../lib/web/fetch/response.js'
+
+const settings = {
+  small: 2 << 8,
+  middle: 2 << 12,
+  long: 2 << 16
+}
+
+for (const [name, length] of Object.entries(settings)) {
+  const buffer = Buffer.allocUnsafe(length).map(() => (Math.random() * 100) | 0)
+  group(`${name} (length ${length})`, () => {
+    bench('Response#arrayBuffer', async () => {
+      return await new Response(buffer).arrayBuffer()
+    })
+
+    // for comparison
+    bench('Response#text', async () => {
+      return await new Response(buffer).text()
+    })
+  })
+}
+
+await run()

programming_language: TypeScript
filename: body.js
change_type: ModificationType.MODIFY
diff: @@ -309,7 +309,7 @@ function bodyMixinMethods (instance) {
         // Return a Blob whose contents are bytes and type attribute
         // is mimeType.
         return new Blob([bytes], { type: mimeType })
-      }, instance)
+      }, instance, false)
     },
 
     arrayBuffer () {
@@ -318,20 +318,21 @@ function bodyMixinMethods (instance) {
       // given a byte sequence bytes: return a new ArrayBuffer
       // whose contents are bytes.
       return consumeBody(this, (bytes) => {
-        return new Uint8Array(bytes).buffer
-      }, instance)
+        // Note: arrayBuffer already cloned.
+        return bytes.buffer
+      }, instance, true)
     },
 
     text () {
       // The text() method steps are to return the result of running
       // consume body with this and UTF-8 decode.
-      return consumeBody(this, utf8DecodeBytes, instance)
+      return consumeBody(this, utf8DecodeBytes, instance, false)
     },
 
     json () {
       // The json() method steps are to return the result of running
       // consume body with this and parse JSON from bytes.
-      return consumeBody(this, parseJSONFromBytes, instance)
+      return consumeBody(this, parseJSONFromBytes, instance, false)
     },
 
     formData () {
@@ -383,7 +384,7 @@ function bodyMixinMethods (instance) {
         throw new TypeError(
           'Content-Type was not one of "multipart/form-data" or "application/x-www-form-urlencoded".'
         )
-      }, instance)
+      }, instance, false)
     }
   }
 
@@ -399,8 +400,9 @@ function mixinBody (prototype) {
  * @param {Response|Request} object
  * @param {(value: unknown) => unknown} convertBytesToJSValue
  * @param {Response|Request} instance
+ * @param {boolean} [shouldClone]
  */
-async function consumeBody (object, convertBytesToJSValue, instance) {
+async function consumeBody (object, convertBytesToJSValue, instance, shouldClone) {
   webidl.brandCheck(object, instance)
 
   // 1. If object is unusable, then return a promise rejected
@@ -432,13 +434,13 @@ async function consumeBody (object, convertBytesToJSValue, instance) {
   // 5. If object’s body is null, then run successSteps with an
   //    empty byte sequence.
   if (object[kState].body == null) {
-    successSteps(new Uint8Array())
+    successSteps(Buffer.allocUnsafe(0))
     return promise.promise
   }
 
   // 6. Otherwise, fully read object’s body given successSteps,
   //    errorSteps, and object’s relevant global object.
-  await fullyReadBody(object[kState].body, successSteps, errorSteps)
+  await fullyReadBody(object[kState].body, successSteps, errorSteps, shouldClone)
 
   // 7. Return promise.
   return promise.promise

programming_language: JavaScript
filename: util.js
change_type: ModificationType.MODIFY
diff: @@ -1043,7 +1043,7 @@ function iteratorMixin (name, object, kInternalIterator, keyIndex = 0, valueInde
 /**
  * @see https://fetch.spec.whatwg.org/#body-fully-read
  */
-async function fullyReadBody (body, processBody, processBodyError) {
+async function fullyReadBody (body, processBody, processBodyError, shouldClone) {
   // 1. If taskDestination is null, then set taskDestination to
   //    the result of starting a new parallel queue.
 
@@ -1069,8 +1069,7 @@ async function fullyReadBody (body, processBody, processBodyError) {
 
   // 5. Read all bytes from reader, given successSteps and errorSteps.
   try {
-    const result = await readAllBytes(reader)
-    successSteps(result)
+    successSteps(await readAllBytes(reader, shouldClone))
   } catch (e) {
     errorSteps(e)
   }
@@ -1118,8 +1117,9 @@ function isomorphicEncode (input) {
  * @see https://streams.spec.whatwg.org/#readablestreamdefaultreader-read-all-bytes
  * @see https://streams.spec.whatwg.org/#read-loop
  * @param {ReadableStreamDefaultReader} reader
+ * @param {boolean} [shouldClone]
  */
-async function readAllBytes (reader) {
+async function readAllBytes (reader, shouldClone) {
   const bytes = []
   let byteLength = 0
 
@@ -1128,6 +1128,13 @@ async function readAllBytes (reader) {
 
     if (done) {
       // 1. Call successSteps with bytes.
+      if (bytes.length === 1) {
+        const { buffer, byteOffset, byteLength } = bytes[0]
+        if (shouldClone === false) {
+          return Buffer.from(buffer, byteOffset, byteLength)
+        }
+        return Buffer.from(buffer.slice(byteOffset, byteOffset + byteLength), 0, byteLength)
+      }
       return Buffer.concat(bytes, byteLength)
     }
 

programming_language: JavaScript