CVE-2023-26136: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution') (CWE-1321)
value: Versions of the package tough-cookie before 4.1.3 are vulnerable to Prototype Pollution due to improper handling of Cookies when using CookieJar in rejectPublicSuffixes=false mode. This issue arises from the manner in which the objects are initialized.
CWE description: The product receives input from an upstream component that specifies attributes that are to be initialized or updated in an object, but it does not properly control modifications of attributes of the object prototype.
CVSS v3 base score: 9.8
Published: 2023-07-01T05:15Z
Repository: https://github.com/salesforce/tough-cookie
Language: JavaScript
Files changed: lib/memstore.js, test/cookie_jar_test.js
Commit date: 2023-06-05 08:13:22 -0700
Commit message: Prevent prototype pollution in cookie memstore (#283)

All occurrences of new object creation in `memstore.js` have been changed from `{}` (i.e.; `Object.create(Object.prototype)` to `Object.create(null)` so that we are using object instances that do not have a prototype property that can be polluted.

@fixes #282
Diff: diff --git a/lib/memstore.js b/lib/memstore.js
index 001ca93..f313bbf 100644
--- a/lib/memstore.js
+++ b/lib/memstore.js
@@ -37,11 +37,11 @@ const { getCustomInspectSymbol, getUtilInspect } = require("./utilHelper");
 
 class MemoryCookieStore extends Store {
   constructor() {
     super();
     this.synchronous = true;
-    this.idx = {};
+    this.idx = Object.create(null);
     const customInspectSymbol = getCustomInspectSymbol();
     if (customInspectSymbol) {
       this[customInspectSymbol] = this.inspect;
     }
   }
@@ -109,14 +109,14 @@ class MemoryCookieStore extends Store {
     cb(null, results);
   }
 
   putCookie(cookie, cb) {
     if (!this.idx[cookie.domain]) {
-      this.idx[cookie.domain] = {};
+      this.idx[cookie.domain] = Object.create(null);
     }
     if (!this.idx[cookie.domain][cookie.path]) {
-      this.idx[cookie.domain][cookie.path] = {};
+      this.idx[cookie.domain][cookie.path] = Object.create(null);
     }
     this.idx[cookie.domain][cookie.path][cookie.key] = cookie;
     cb(null);
   }
   updateCookie(oldCookie, newCookie, cb) {
@@ -144,11 +144,11 @@ class MemoryCookieStore extends Store {
       }
     }
     return cb(null);
   }
   removeAllCookies(cb) {
-    this.idx = {};
+    this.idx = Object.create(null);
     return cb(null);
   }
   getAllCookies(cb) {
     const cookies = [];
     const idx = this.idx;
@@ -194,13 +194,13 @@ class MemoryCookieStore extends Store {
 exports.MemoryCookieStore = MemoryCookieStore;
 
 function inspectFallback(val) {
   const domains = Object.keys(val);
   if (domains.length === 0) {
-    return "{}";
+    return "[Object: null prototype] {}";
   }
-  let result = "{\n";
+  let result = "[Object: null prototype] {\n";
   Object.keys(val).forEach((domain, i) => {
     result += formatDomain(domain, val[domain]);
     if (i < domains.length - 1) {
       result += ",";
     }
@@ -210,11 +210,11 @@ function inspectFallback(val) {
   return result;
 }
 
 function formatDomain(domainName, domainValue) {
   const indent = "  ";
-  let result = `${indent}'${domainName}': {\n`;
+  let result = `${indent}'${domainName}': [Object: null prototype] {\n`;
   Object.keys(domainValue).forEach((path, i, paths) => {
     result += formatPath(path, domainValue[path]);
     if (i < paths.length - 1) {
       result += ",";
     }
@@ -224,11 +224,11 @@ function formatDomain(domainName, domainValue) {
   return result;
 }
 
 function formatPath(pathName, pathValue) {
   const indent = "    ";
-  let result = `${indent}'${pathName}': {\n`;
+  let result = `${indent}'${pathName}': [Object: null prototype] {\n`;
   Object.keys(pathValue).forEach((cookieName, i, cookieNames) => {
     const cookie = pathValue[cookieName];
     result += `      ${cookieName}: ${cookie.inspect()}`;
     if (i < cookieNames.length - 1) {
       result += ",";
diff --git a/test/cookie_jar_test.js b/test/cookie_jar_test.js
index 676e50d..50205bc 100644
--- a/test/cookie_jar_test.js
+++ b/test/cookie_jar_test.js
@@ -807,6 +807,31 @@ vows
           assert(cookies == undefined);
         }
       }
     }
   })
+  .addBatch({
+    "Issue #282 - Prototype pollution": {
+      "when setting a cookie with the domain __proto__": {
+        topic: function() {
+          const jar = new tough.CookieJar(undefined, {
+            rejectPublicSuffixes: false
+          });
+          // try to pollute the prototype
+          jar.setCookieSync(
+            "Slonser=polluted; Domain=__proto__; Path=/notauth",
+            "https://__proto__/admin"
+          );
+          jar.setCookieSync(
+            "Auth=Lol; Domain=google.com; Path=/notauth",
+            "https://google.com/"
+          );
+          this.callback();
+        },
+        "results in a cookie that is not affected by the attempted prototype pollution": function() {
+          const pollutedObject = {};
+          assert(pollutedObject["/notauth"] === undefined);
+        }
+      }
+    }
+  })
   .export(module);

Vulnerable code:     this.idx = {};
      this.idx[cookie.domain] = {};
      this.idx[cookie.domain][cookie.path] = {};
    this.idx = {};
    return "{}";
  let result = "{\n";
  let result = `${indent}'${domainName}': {\n`;
  let result = `${indent}'${pathName}': {\n`;
Fixed code:     this.idx = Object.create(null);
      this.idx[cookie.domain] = Object.create(null);
      this.idx[cookie.domain][cookie.path] = Object.create(null);
    this.idx = Object.create(null);
    return "[Object: null prototype] {}";
  let result = "[Object: null prototype] {\n";
  let result = `${indent}'${domainName}': [Object: null prototype] {\n`;
  let result = `${indent}'${pathName}': [Object: null prototype] {\n`;
  .addBatch({
    "Issue #282 - Prototype pollution": {
      "when setting a cookie with the domain __proto__": {
        topic: function() {
          const jar = new tough.CookieJar(undefined, {
            rejectPublicSuffixes: false
          });
          // try to pollute the prototype
          jar.setCookieSync(
            "Slonser=polluted; Domain=__proto__; Path=/notauth",
            "https://__proto__/admin"
          );
          jar.setCookieSync(
            "Auth=Lol; Domain=google.com; Path=/notauth",
            "https://google.com/"
          );
          this.callback();
        },
        "results in a cookie that is not affected by the attempted prototype pollution": function() {
          const pollutedObject = {};
          assert(pollutedObject["/notauth"] === undefined);
        }
      }
    }
  })