CVE-2023-45143: Exposure of Sensitive Information to an Unauthorized Actor (CWE-200)
value: Undici is an HTTP/1.1 client written from scratch for Node.js. Prior to version 5.26.2, Undici already cleared Authorization headers on cross-origin redirects, but did not clear `Cookie` headers. By design, `cookie` headers are forbidden request headers, disallowing them to be set in RequestInit.headers in browser environments. Since undici handles headers more liberally than the spec, there was a disconnect from the assumptions the spec made, and undici's implementation of fetch. As such this may lead to accidental leakage of cookie to a third-party site or a malicious attacker who can control the redirection target (ie. an open redirector) to leak the cookie to the third party site. This was patched in version 5.26.2. There are no known workarounds.
CWE description: The product exposes sensitive information to an actor that is not explicitly authorized to have access to that information.
CVSS v3 base score: 3.5
Published: 2023-10-12T17:15Z
Repository: https://github.com/nodejs/undici
Language: JavaScript
Files changed: lib/fetch/index.js, test/fetch/redirect-cross-origin-header.js
Commit date: 2023-10-11 20:56:38 +0200
Commit message: Merge pull request from GHSA-wqq4-5wpv-mx2g

* fix: delete 'cookie' and 'host' headers on cross-origin redirect

* apply suggestion
Diff: diff --git a/lib/fetch/index.js b/lib/fetch/index.js
index c89c9b7f..5323c30a 100644
--- a/lib/fetch/index.js
+++ b/lib/fetch/index.js
@@ -1198,10 +1198,14 @@ async function httpRedirectFetch (fetchParams, response) {
   //     origin, then for each headerName of CORS non-wildcard request-header name,
   //     delete headerName from request’s header list.
   if (!sameOrigin(requestCurrentURL(request), locationURL)) {
     // https://fetch.spec.whatwg.org/#cors-non-wildcard-request-header-name
     request.headersList.delete('authorization')
+
+    // "Cookie" and "Host" are forbidden request-headers, which undici doesn't implement.
+    request.headersList.delete('cookie')
+    request.headersList.delete('host')
   }
 
   // 14. If request’s body is non-null, then set request’s body to the first return
   // value of safely extracting request’s body’s source.
   if (request.body != null) {
diff --git a/test/fetch/redirect-cross-origin-header.js b/test/fetch/redirect-cross-origin-header.js
new file mode 100644
index 00000000..fca48c44
--- /dev/null
+++ b/test/fetch/redirect-cross-origin-header.js
@@ -0,0 +1,48 @@
+'use strict'
+
+const { test } = require('tap')
+const { createServer } = require('http')
+const { once } = require('events')
+const { fetch } = require('../..')
+
+test('Cross-origin redirects clear forbidden headers', async (t) => {
+  t.plan(5)
+
+  const server1 = createServer((req, res) => {
+    t.equal(req.headers.cookie, undefined)
+    t.equal(req.headers.authorization, undefined)
+
+    res.end('redirected')
+  }).listen(0)
+
+  const server2 = createServer((req, res) => {
+    t.equal(req.headers.authorization, 'test')
+    t.equal(req.headers.cookie, 'ddd=dddd')
+
+    res.writeHead(302, {
+      ...req.headers,
+      Location: `http://localhost:${server1.address().port}`
+    })
+    res.end()
+  }).listen(0)
+
+  t.teardown(() => {
+    server1.close()
+    server2.close()
+  })
+
+  await Promise.all([
+    once(server1, 'listening'),
+    once(server2, 'listening')
+  ])
+
+  const res = await fetch(`http://localhost:${server2.address().port}`, {
+    headers: {
+      Authorization: 'test',
+      Cookie: 'ddd=dddd'
+    }
+  })
+
+  const text = await res.text()
+  t.equal(text, 'redirected')
+})

Fixed code: 
    // "Cookie" and "Host" are forbidden request-headers, which undici doesn't implement.
    request.headersList.delete('cookie')
    request.headersList.delete('host')
'use strict'

const { test } = require('tap')
const { createServer } = require('http')
const { once } = require('events')
const { fetch } = require('../..')

test('Cross-origin redirects clear forbidden headers', async (t) => {
  t.plan(5)

  const server1 = createServer((req, res) => {
    t.equal(req.headers.cookie, undefined)
    t.equal(req.headers.authorization, undefined)

    res.end('redirected')
  }).listen(0)

  const server2 = createServer((req, res) => {
    t.equal(req.headers.authorization, 'test')
    t.equal(req.headers.cookie, 'ddd=dddd')

    res.writeHead(302, {
      ...req.headers,
      Location: `http://localhost:${server1.address().port}`
    })
    res.end()
  }).listen(0)

  t.teardown(() => {
    server1.close()
    server2.close()
  })

  await Promise.all([
    once(server1, 'listening'),
    once(server2, 'listening')
  ])

  const res = await fetch(`http://localhost:${server2.address().port}`, {
    headers: {
      Authorization: 'test',
      Cookie: 'ddd=dddd'
    }
  })

  const text = await res.text()
  t.equal(text, 'redirected')
})