CVE-2022-0536: Improper Removal of Sensitive Information Before Storage or Transfer (CWE-212)
value: Improper Removal of Sensitive Information Before Storage or Transfer in NPM follow-redirects prior to 1.14.8.


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: 5.9
Published: 2022-02-09T11:15Z
Repository: https://github.com/follow-redirects/follow-redirects
Language: JavaScript
Files changed: index.js, test/test.js
Commit date: 2022-02-08 11:02:43 +0000
Commit message: Drop confidential headers across schemes.

Diff: diff --git a/index.js b/index.js
index ecb05f7..02f37ea 100644
--- a/index.js
+++ b/index.js
@@ -390,12 +390,13 @@ RedirectableRequest.prototype._processResponse = function (response) {
     debug("redirecting to", redirectUrl);
     this._isRedirect = true;
     var redirectUrlParts = url.parse(redirectUrl);
     Object.assign(this._options, redirectUrlParts);
 
-    // Drop the confidential headers when redirecting to another domain
-    if (!(redirectUrlParts.host === currentHost || isSubdomainOf(redirectUrlParts.host, currentHost))) {
+    // Drop confidential headers when redirecting to another scheme:domain
+    if (redirectUrlParts.protocol !== currentUrlParts.protocol ||
+       !isSameOrSubdomain(redirectUrlParts.host, currentHost)) {
       removeMatchingHeaders(/^(?:authorization|cookie)$/i, this._options.headers);
     }
 
     // Evaluate the beforeRedirect callback
     if (typeof this._options.beforeRedirect === "function") {
@@ -557,11 +558,14 @@ function abortRequest(request) {
   }
   request.on("error", noop);
   request.abort();
 }
 
-function isSubdomainOf(subdomain, domain) {
+function isSameOrSubdomain(subdomain, domain) {
+  if (subdomain === domain) {
+    return true;
+  }
   const dot = subdomain.length - domain.length - 1;
   return dot > 0 && subdomain[dot] === "." && subdomain.endsWith(domain);
 }
 
 // Exports
diff --git a/test/test.js b/test/test.js
index 9e8e45f..7d2f122 100644
--- a/test/test.js
+++ b/test/test.js
@@ -1520,10 +1520,40 @@ describe("follow-redirects", function () {
             assert.equal(body.host, "127.0.0.1:3600");
             assert.equal(body[header.toLowerCase()], undefined);
           });
       });
     });
+
+    it("drops the header when redirected to a different scheme", function () {
+      app.get("/a", redirectsTo(302, "http://localhost:3601/b"));
+      app.get("/b", function (req, res) {
+        res.end(JSON.stringify(req.headers));
+      });
+
+      var opts = url.parse("https://localhost:3601/a");
+      opts.ca = ca;
+      opts.headers = {};
+      opts.headers[header] = "the header value";
+
+      // Intercept the scheme
+      opts.beforeRedirect = function (options) {
+        assert.equal(options.protocol, "http:");
+        options.protocol = "https:";
+      };
+
+      return server.start(httpsOptions(app))
+        .then(asPromise(function (resolve, reject) {
+          https.get(opts, resolve).on("error", reject);
+        }))
+        .then(asPromise(function (resolve, reject, res) {
+          res.pipe(concat({ encoding: "string" }, resolve)).on("error", reject);
+        }))
+        .then(function (str) {
+          var body = JSON.parse(str);
+          assert.equal(body[header.toLowerCase()], undefined);
+        });
+    });
   });
 
   describe("when the followRedirects option is set to false", function () {
     it("does not redirect", function () {
       app.get("/a", redirectsTo(302, "/b"));

Vulnerable code:     // Drop the confidential headers when redirecting to another domain
    if (!(redirectUrlParts.host === currentHost || isSubdomainOf(redirectUrlParts.host, currentHost))) {
function isSubdomainOf(subdomain, domain) {
Fixed code:     // Drop confidential headers when redirecting to another scheme:domain
    if (redirectUrlParts.protocol !== currentUrlParts.protocol ||
       !isSameOrSubdomain(redirectUrlParts.host, currentHost)) {
function isSameOrSubdomain(subdomain, domain) {
  if (subdomain === domain) {
    return true;
  }

    it("drops the header when redirected to a different scheme", function () {
      app.get("/a", redirectsTo(302, "http://localhost:3601/b"));
      app.get("/b", function (req, res) {
        res.end(JSON.stringify(req.headers));
      });

      var opts = url.parse("https://localhost:3601/a");
      opts.ca = ca;
      opts.headers = {};
      opts.headers[header] = "the header value";

      // Intercept the scheme
      opts.beforeRedirect = function (options) {
        assert.equal(options.protocol, "http:");
        options.protocol = "https:";
      };

      return server.start(httpsOptions(app))
        .then(asPromise(function (resolve, reject) {
          https.get(opts, resolve).on("error", reject);
        }))
        .then(asPromise(function (resolve, reject, res) {
          res.pipe(concat({ encoding: "string" }, resolve)).on("error", reject);
        }))
        .then(function (str) {
          var body = JSON.parse(str);
          assert.equal(body[header.toLowerCase()], undefined);
        });
    });