CVE-2022-24719 (JavaScript, published 2022-03-01T21:15Z)
Description: 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.
Severity: MEDIUM (CVSS v3 base score: 6.1)
Weakness: CWE-212 \u2014 Improper Removal of Sensitive Information Before Storage or Transfer
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.
Repository: https://github.com/fluture-js/fluture-node
Files changed:
index.js
test/index.js
Commit message: Update redirection strategies to drop confidential headers


Vulnerable code:
// mergeUrls :: Url -> Any -> String
const mergeUrls = base => input => (
  const newUrl = mergeUrls (oldUrl) (location);
  return (Request (Request.options (original))
                  (newUrl)
                  (Request.body (original)));
//. The original request method and body are discarded, but all the options
//. are preserved.
                 (fn.Request ({}) ('ftp://xxx/') (fn.emptyStream)),
                 (fn.Request ({}) ('ftp://xxx/') (fn.emptyStream)),
                 (fn.Request ({method: 'GET'}) ('ftp://xxx/') (fn.emptyStream)),

Fixed code:
// mergeUrls :: (Url, Any) -> String
const mergeUrls = (base, input) => (
// 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);
};

// confidentialHeaders :: Array String
const confidentialHeaders = [
  'authorization',
  'cookie',
];

//. 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.
//.
  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 ()))
  ));
//. 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.
//.
//. 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.
                 (fn.Request ({headers: {}}) ('ftp://xxx/') (fn.emptyStream)),
  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)),
                 (fn.Request ({headers: {}}) ('ftp://xxx/') (fn.emptyStream)),
                 (fn.Request ({method: 'GET', headers: {}}) ('ftp://xxx/') (fn.emptyStream)),