CVE-2022-24719 (published 2022-03-01T21:15Z)

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.

Severity: MEDIUM
CVSS v2 base score: 5.8 (AV:N/AC:M/Au:N/C:P/I:P/A:N)
CVSS v3 base score: 6.1, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N)

Weakness information:
cwe_id: CWE-212
cwe_name: Improper Removal of Sensitive Information Before Storage or Transfer
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.
extended_description: 
            Resources that may contain sensitive data include documents, packets, messages, databases, etc. While this data may be useful to an individual user or small set of users who share the resource, it may need to be removed before the resource can be shared outside of the trusted group. The process of removal is sometimes called cleansing or scrubbing.
            For example, a product for editing documents might not remove sensitive data such as reviewer comments or the local pathname where the document is stored. Or, a proxy might not remove an internal IP address from headers before making an outgoing request to an Internet site.
         
         

Fix information:
repo_url: https://github.com/fluture-js/fluture-node
commit_details: repo_url: https://github.com/fluture-js/fluture-node
author: Aldwin Vlasblom
author_date: 2022-03-01 12:39:18+01:00
msg: Fix notion of cross-origin to include scheme changes
repository: repo_url: https://github.com/fluture-js/fluture-node
repo_name: fluture-js/fluture-node
description: FP-style HTTP and streaming utils for Node based on Fluture
repo_language: JavaScript
owner: fluture-js
file_changes: filename: index.js
change_type: ModificationType.MODIFY
diff: @@ -541,11 +541,12 @@ const mergeUrls = (base, input) => (
   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)
@@ -583,7 +584,7 @@ export const redirectAnyRequest = response => {
                           (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 ()))
   ));
 };

programming_language: JavaScript
filename: index.js
change_type: ModificationType.MODIFY
diff: @@ -284,6 +284,11 @@ test ('redirectAnyRequest', () => Promise.all ([
                                          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 ([

programming_language: JavaScript
repo_url: https://github.com/fluture-js/fluture-node
commit_details: repo_url: https://github.com/fluture-js/fluture-node
author: Aldwin Vlasblom
author_date: 2022-02-26 16:58:53+01:00
msg: Update redirection strategies to drop confidential headers
repository: repo_url: https://github.com/fluture-js/fluture-node
repo_name: fluture-js/fluture-node
description: FP-style HTTP and streaming utils for Node based on Fluture
repo_language: JavaScript
owner: fluture-js
file_changes: filename: index.js
change_type: ModificationType.MODIFY
diff: @@ -534,13 +534,20 @@ export const matchStatus = f => fs => res => {
   return (hasProp (statusCode) (fs) ? fs[statusCode] : f) (res);
 };
 
-// mergeUrls :: Url -> Any -> String
-const mergeUrls = base => input => (
+// mergeUrls :: (Url, Any) -> String
+const mergeUrls = (base, input) => (
   typeof input === 'string' ?
   new URL (input, base).href :
   base
 );
 
+// sameHost :: (Url, Url) -> Boolean
+const sameHost = (parent, child) => {
+  const p = new URL (parent);
+  const c = new URL (child);
+  return p.host === c.host || c.host.endsWith ('.' + p.host);
+};
+
 // overHeaders :: (Request, Array2 String String -> Array2 String String)
 //             -> Request
 const overHeaders = (request, f) => {
@@ -551,21 +558,34 @@ const overHeaders = (request, f) => {
                  (Request.body (request));
 };
 
+// confidentialHeaders :: Array String
+const confidentialHeaders = [
+  'authorization',
+  'cookie',
+];
+
 //# redirectAnyRequest :: Response -> Request
 //.
 //. A redirection strategy that simply reissues the original Request to the
 //. Location specified in the given Response.
 //.
+//. If the new location is on an external host, then any confidential headers
+//. (such as the cookie header) will be dropped from the new request.
+//.
 //. Used in the [`defaultRedirectionPolicy`](#defaultRedirectionPolicy) and
 //. the [`aggressiveRedirectionPolicy`](#aggressiveRedirectionPolicy).
 export const redirectAnyRequest = response => {
   const {headers: {location}} = Response.message (response);
   const original = Response.request (response);
   const oldUrl = Request.url (original);
-  const newUrl = mergeUrls (oldUrl) (location);
-  return (Request (Request.options (original))
-                  (newUrl)
-                  (Request.body (original)));
+  const newUrl = mergeUrls (oldUrl, location);
+  const request = Request (Request.options (original))
+                          (newUrl)
+                          (Request.body (original));
+
+  return sameHost (oldUrl, newUrl) ? request : overHeaders (request, xs => (
+    xs.filter (([name]) => !confidentialHeaders.includes (name.toLowerCase ()))
+  ));
 };
 
 //# redirectIfGetMethod :: Response -> Request
@@ -574,6 +594,9 @@ export const redirectAnyRequest = response => {
 //. Location specified in the given Response, but only if the original request
 //. was using the GET method.
 //.
+//. If the new location is on an external host, then any confidential headers
+//. (such as the cookie header) will be dropped from the new request.
+//.
 //. Used in [`followRedirectsStrict`](#followRedirectsStrict).
 export const redirectIfGetMethod = response => {
   const {method} = cleanRequestOptions (Response.request (response));
@@ -590,8 +613,10 @@ export const redirectIfGetMethod = response => {
 //. request to the Location specified in the given Response. If the response
 //. does not contain a valid location, the request is not redirected.
 //.
-//. The original request method and body are discarded, but all the options
-//. are preserved.
+//. The original request method and body are discarded, but other options
+//. are preserved. If the new location is on an external host, then any
+//. confidential headers (such as the cookie header) will be dropped from the
+//. new request.
 //.
 //. Used in the [`defaultRedirectionPolicy`](#defaultRedirectionPolicy) and
 //. the [`aggressiveRedirectionPolicy`](#aggressiveRedirectionPolicy).

programming_language: JavaScript
filename: index.js
change_type: ModificationType.MODIFY
diff: @@ -259,18 +259,38 @@ test ('redirectAnyRequest', () => Promise.all ([
   assertResolves (fl.map (fn.redirectAnyRequest) (mockResponse ({})))
                  (getRequest),
   assertResolves (fl.map (fn.redirectAnyRequest) (getResponse (301) ('ftp://xxx')))
-                 (fn.Request ({}) ('ftp://xxx/') (fn.emptyStream)),
+                 (fn.Request ({headers: {}}) ('ftp://xxx/') (fn.emptyStream)),
   assertResolves (fl.map (fn.redirectAnyRequest) (getResponse (301) ('/echo')))
                  (fn.Request ({}) ('https://example.com/echo') (fn.emptyStream)),
   assertResolves (fl.map (fn.redirectAnyRequest) (postResponse (301) ('/echo')))
                  (fn.Request ({method: 'POST'}) ('https://example.com/echo') (fn.streamOf (Buffer.from ('test')))),
+  assertResolves (fl.map (fn.redirectAnyRequest)
+                         (mockResponse ({code: 301,
+                                         headers: {location: 'https://example.com/path'},
+                                         request: fn.Request ({headers: {cookie: 'yum'}}) ('https://example.com') (fn.emptyStream)})))
+                 (fn.Request ({headers: {cookie: 'yum'}}) ('https://example.com/path') (fn.emptyStream)),
+  assertResolves (fl.map (fn.redirectAnyRequest)
+                         (mockResponse ({code: 301,
+                                         headers: {location: 'https://sub.example.com/'},
+                                         request: fn.Request ({headers: {cookie: 'yum'}}) ('https://example.com') (fn.emptyStream)})))
+                 (fn.Request ({headers: {cookie: 'yum'}}) ('https://sub.example.com/') (fn.emptyStream)),
+  assertResolves (fl.map (fn.redirectAnyRequest)
+                         (mockResponse ({code: 301,
+                                         headers: {location: 'https://bigsub.example.com/'},
+                                         request: fn.Request ({headers: {cookie: 'yum'}}) ('https://example.com') (fn.emptyStream)})))
+                 (fn.Request ({headers: {cookie: 'yum'}}) ('https://bigsub.example.com/') (fn.emptyStream)),
+  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)),
 ]));
 
 test ('redirectIfGetMethod', () => Promise.all ([
   assertResolves (fl.map (fn.redirectIfGetMethod) (mockResponse ({})))
                  (getRequest),
   assertResolves (fl.map (fn.redirectIfGetMethod) (getResponse (301) ('ftp://xxx')))
-                 (fn.Request ({}) ('ftp://xxx/') (fn.emptyStream)),
+                 (fn.Request ({headers: {}}) ('ftp://xxx/') (fn.emptyStream)),
   assertResolves (fl.map (fn.redirectIfGetMethod) (getResponse (301) ('/echo')))
                  (fn.Request ({}) ('https://example.com/echo') (fn.emptyStream)),
   assertResolves (fl.map (fn.redirectIfGetMethod) (postResponse (301) ('/echo')))
@@ -281,7 +301,7 @@ test ('redirectUsingGetMethod', () => Promise.all ([
   assertResolves (fl.map (fn.redirectUsingGetMethod) (mockResponse ({})))
                  (fn.Request ({method: 'GET'}) ('https://example.com') (fn.emptyStream)),
   assertResolves (fl.map (fn.redirectUsingGetMethod) (getResponse (301) ('ftp://xxx')))
-                 (fn.Request ({method: 'GET'}) ('ftp://xxx/') (fn.emptyStream)),
+                 (fn.Request ({method: 'GET', headers: {}}) ('ftp://xxx/') (fn.emptyStream)),
   assertResolves (fl.map (fn.redirectUsingGetMethod) (getResponse (200) ('/echo')))
                  (fn.Request ({method: 'GET'}) ('https://example.com/echo') (fn.emptyStream)),
   assertResolves (fl.map (fn.redirectUsingGetMethod) (postResponse (200) ('/echo')))

programming_language: JavaScript