CVE-2022-24719: Improper Removal of Sensitive Information Before Storage or Transfer (CWE-212)
value: Fluture-Node is a FP-style HTTP and streaming utils for Node based on Fluture. Using `followRedirects` or `followRedirectsWith` with any of the redirection strategies built into fluture-node 4.0.0 or 4.0.1, paired with a request that includes confidential headers such as Authorization or Cookie, exposes you to a vulnerability where, if the destination server were to redirect the request to a server on a third-party domain, or the same domain over unencrypted HTTP, the headers would be included in the follow-up request and be exposed to the third party, or potential http traffic sniffing. The redirection strategies made available in version 4.0.2 automatically redact confidential headers when a redirect is followed across to another origin. A workaround has been identified by using a custom redirection strategy via the `followRedirectsWith` function. The custom strategy can be based on the new strategies available in fluture-node@4.0.2.
CWE description: The product stores, transfers, or shares a resource that contains sensitive information, but it does not properly remove that information before the product makes the resource available to unauthorized actors.
CVSS v3 base score: 6.1
Published: 2022-03-01T21:15Z
Repository: https://github.com/fluture-js/fluture-node
Language: JavaScript
Files changed: index.js, test/index.js
Commit date: 2022-03-01 12:39:18 +0100
Commit message: Fix notion of cross-origin to include scheme changes

Diff: diff --git a/index.js b/index.js
index 08eca2c..108c9ea 100644
--- a/index.js
+++ b/index.js
@@ -539,15 +539,16 @@ const mergeUrls = (base, input) => (
   typeof input === 'string' ?
   new URL (input, base).href :
   base
 );
 
-// sameHost :: (Url, Url) -> Boolean
-const sameHost = (parent, child) => {
+// sameOrigin :: (Url, Url) -> Boolean
+const sameOrigin = (parent, child) => {
   const p = new URL (parent);
   const c = new URL (child);
-  return p.host === c.host || c.host.endsWith ('.' + p.host);
+  return (p.protocol === c.protocol || c.protocol === 'https:') &&
+         (p.host === c.host || c.host.endsWith ('.' + p.host));
 };
 
 // overHeaders :: (Request, Array2 String String -> Array2 String String)
 //             -> Request
 const overHeaders = (request, f) => {
@@ -581,11 +582,11 @@ export const redirectAnyRequest = response => {
   const newUrl = mergeUrls (oldUrl, location);
   const request = Request (Request.options (original))
                           (newUrl)
                           (Request.body (original));
 
-  return sameHost (oldUrl, newUrl) ? request : overHeaders (request, xs => (
+  return sameOrigin (oldUrl, newUrl) ? request : overHeaders (request, xs => (
     xs.filter (([name]) => !confidentialHeaders.includes (name.toLowerCase ()))
   ));
 };
 
 //# redirectIfGetMethod :: Response -> Request
diff --git a/test/index.js b/test/index.js
index 9ef837b..c8a42e1 100644
--- a/test/index.js
+++ b/test/index.js
@@ -282,10 +282,15 @@ test ('redirectAnyRequest', () => Promise.all ([
   assertResolves (fl.map (fn.redirectAnyRequest)
                          (mockResponse ({code: 301,
                                          headers: {location: 'https://elsewhere.com/'},
                                          request: fn.Request ({headers: {cookie: 'yum'}}) ('https://example.com') (fn.emptyStream)})))
                  (fn.Request ({headers: {}}) ('https://elsewhere.com/') (fn.emptyStream)),
+  assertResolves (fl.map (fn.redirectAnyRequest)
+                         (mockResponse ({code: 301,
+                                         headers: {location: 'http://example.com/'},
+                                         request: fn.Request ({headers: {cookie: 'yum'}}) ('https://example.com') (fn.emptyStream)})))
+                 (fn.Request ({headers: {}}) ('http://example.com/') (fn.emptyStream)),
 ]));
 
 test ('redirectIfGetMethod', () => Promise.all ([
   assertResolves (fl.map (fn.redirectIfGetMethod) (mockResponse ({})))
                  (getRequest),

Vulnerable code: // sameHost :: (Url, Url) -> Boolean
const sameHost = (parent, child) => {
  return p.host === c.host || c.host.endsWith ('.' + p.host);
  return sameHost (oldUrl, newUrl) ? request : overHeaders (request, xs => (
Fixed code: // sameOrigin :: (Url, Url) -> Boolean
const sameOrigin = (parent, child) => {
  return (p.protocol === c.protocol || c.protocol === 'https:') &&
         (p.host === c.host || c.host.endsWith ('.' + p.host));
  return sameOrigin (oldUrl, newUrl) ? request : overHeaders (request, xs => (
  assertResolves (fl.map (fn.redirectAnyRequest)
                         (mockResponse ({code: 301,
                                         headers: {location: 'http://example.com/'},
                                         request: fn.Request ({headers: {cookie: 'yum'}}) ('https://example.com') (fn.emptyStream)})))
                 (fn.Request ({headers: {}}) ('http://example.com/') (fn.emptyStream)),