CVE-2022-0654: Insufficient Information (NVD-CWE-noinfo)
value: Exposure of Sensitive Information to an Unauthorized Actor in GitHub repository fgribreau/node-request-retry prior to 7.0.0.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
CVSS v3 base score: 7.5
Published: 2022-02-23T00:15Z
Repository: https://github.com/fgribreau/node-request-retry
Language: JavaScript
Files changed: index.js
Commit date: 2022-02-16 00:20:12 +0400
Commit message: Prevent Cookie & Authorization Headers from being forwarded when the URL redirects to another domain (information leak) #137
Diff: diff --git a/index.js b/index.js
index 497d83d..9d9685b 100644
--- a/index.js
+++ b/index.js
@@ -9,10 +9,12 @@
  */
 var extend = require('extend');
 var request = require('request');
 var RetryStrategies = require('./strategies');
 var _ = require('lodash');
+var url = require('url');
+var querystring = require("querystring");
 
 var DEFAULTS = {
   maxAttempts: 5, // try 5 times
   retryDelay: 5000, // wait for 5s before trying again
   fullResponse: true, // resolve promise with the full response object
@@ -22,10 +24,46 @@ var DEFAULTS = {
 // Default promise factory which use bluebird
 function defaultPromiseFactory(resolver) {
   return new Promise(resolver);
 }
 
+// Prevent Cookie & Authorization Headers from being forwarded 
+// when the URL redirects to another domain (information leak) #137 
+function sanitizeHeaders(options) {
+  
+  const HEADERS_TO_IGNORE = ["cookie", "authorization"];
+
+  const urlObject = url.parse(options.url)
+  const queryObject = querystring.parse(urlObject.query);
+  
+  const hasExternalLink = Object.keys(queryObject).reduce(function(acc, cur) {
+    
+    let qUrl = url.parse(queryObject[cur]);
+
+    // external link if protocol || host || port is different
+    if(!!qUrl.host && (qUrl.protocol !== urlObject.protocol || qUrl.host !== urlObject.host || qUrl.port !== urlObject.port) ) {
+      acc = true;
+    }
+    
+    return acc;
+
+  }, false);
+
+  if (hasExternalLink && options.hasOwnProperty("headers") && typeof(options.headers) === "object") {
+    
+    // if External Link: remove Cookie and Authorization from Headers
+    Object.keys(options.headers).filter(function(key) {
+      return HEADERS_TO_IGNORE.includes(key.toLowerCase())
+    }).map(function(key) {
+      return delete options.headers[key]
+    });
+
+  }
+
+  return options;
+}
+
 function _cloneOptions(options) {
   const cloned = {};
   for (let key in options) {
     if (options.hasOwnProperty(key)) {
       cloned[key] = key === 'agent' ? options[key] : _.cloneDeep(options[key]);
@@ -83,11 +121,11 @@ function Request(url, options, f, retryConfig) {
 
   /**
    * Option object
    * @type {Object}
    */
-  this.options = options;
+  this.options = sanitizeHeaders(options);
 
   /**
    * Return true if the request should be retried
    * @type {Function} (err, response, body, options) -> [Boolean, Object (optional)]
    */

Vulnerable code:   this.options = options;
Fixed code: var url = require('url');
var querystring = require("querystring");
// Prevent Cookie & Authorization Headers from being forwarded 
// when the URL redirects to another domain (information leak) #137 
function sanitizeHeaders(options) {
  
  const HEADERS_TO_IGNORE = ["cookie", "authorization"];

  const urlObject = url.parse(options.url)
  const queryObject = querystring.parse(urlObject.query);
  
  const hasExternalLink = Object.keys(queryObject).reduce(function(acc, cur) {
    
    let qUrl = url.parse(queryObject[cur]);

    // external link if protocol || host || port is different
    if(!!qUrl.host && (qUrl.protocol !== urlObject.protocol || qUrl.host !== urlObject.host || qUrl.port !== urlObject.port) ) {
      acc = true;
    }
    
    return acc;

  }, false);

  if (hasExternalLink && options.hasOwnProperty("headers") && typeof(options.headers) === "object") {
    
    // if External Link: remove Cookie and Authorization from Headers
    Object.keys(options.headers).filter(function(key) {
      return HEADERS_TO_IGNORE.includes(key.toLowerCase())
    }).map(function(key) {
      return delete options.headers[key]
    });

  }

  return options;
}

  this.options = sanitizeHeaders(options);