CVE-2022-21169: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution') (CWE-1321)
value: The package express-xss-sanitizer before 1.1.3 are vulnerable to Prototype Pollution via the allowedTags attribute, allowing the attacker to bypass xss sanitization.
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: 6.1
Published: 2022-09-26T05:15Z
Repository: https://github.com/AhmedAdelFahim/express-xss-sanitizer
Language: JavaScript
Files changed: .eslintrc.json, lib/sanitize.js, test/test.js
Commit date: 2022-09-20 22:09:26 +0200
Commit message: fix XSS bypass by using prototype pollution issue.

Diff: diff --git a/.eslintrc.json b/.eslintrc.json
index c581a21..04c375c 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -19,8 +19,9 @@
         "no-param-reassign": "off",
         "comma-dangle": "error",
         "prettier/prettier": ["error"],
         "quotes": "off",
         "strict": "off",
-        "prefer-arrow-callback":"off"
+        "prefer-arrow-callback":"off",
+        "operator-linebreak": "off"
     }
 }
diff --git a/lib/sanitize.js b/lib/sanitize.js
index c4ac5ea..53ae75f 100644
--- a/lib/sanitize.js
+++ b/lib/sanitize.js
@@ -2,16 +2,23 @@
 
 const sanitizeHtml = require("sanitize-html");
 
 const initializeOptions = (options) => {
   const sanitizerOptions = {};
-  if (Array.isArray(options.allowedTags) && options.allowedTags.length > 0) {
+  if (
+    Object.hasOwn(options, "allowedTags") &&
+    Array.isArray(options.allowedTags) &&
+    options.allowedTags.length > 0
+  ) {
     sanitizerOptions.allowedTags = options.allowedTags;
   }
   return {
     allowedKeys:
-      (Array.isArray(options.allowedKeys) && options.allowedKeys) || [],
+      (Object.hasOwn(options, "allowedKeys") &&
+        Array.isArray(options.allowedKeys) &&
+        options.allowedKeys) ||
+      [],
     sanitizerOptions,
   };
 };
 
 const sanitize = (options, data) => {
diff --git a/test/test.js b/test/test.js
index 0b90d5c..4dca36a 100644
--- a/test/test.js
+++ b/test/test.js
@@ -1220,6 +1220,36 @@ describe("Express xss Sanitize", function () {
         });
         done();
       });
     });
   });
+
+  describe("Sanitize data with custom options as function", function () {
+    describe("Sanitize simple object", function () {
+      it("should sanitize dirty body.", function (done) {
+        expect(sanitize({
+          a: "<script>Test</script>",
+          b: '<p onclick="return;">Test</p>',
+          c: '<img src="/"/>',
+        }, { allowedKeys: ["c"] })).to.eql({
+          a: "",
+          b: "<p>Test</p>",
+          c: '<img src="/"/>',
+        });
+        done();
+      });
+    });
+
+    describe("XSS bypass by using prototype pollution issue", function () {
+      it("should sanitize dirty data after prototype pollution.", function (done) {
+        // eslint-disable-next-line no-extend-native
+        Object.prototype.allowedTags = ['script'];
+        expect(sanitize({
+          a: "<script>Test</script>",
+        }, {})).to.eql({
+          a: "",
+        });
+        done();
+      });
+    });
+  });
 });

Vulnerable code:         "prefer-arrow-callback":"off"
  if (Array.isArray(options.allowedTags) && options.allowedTags.length > 0) {
      (Array.isArray(options.allowedKeys) && options.allowedKeys) || [],
Fixed code:         "prefer-arrow-callback":"off",
        "operator-linebreak": "off"
  if (
    Object.hasOwn(options, "allowedTags") &&
    Array.isArray(options.allowedTags) &&
    options.allowedTags.length > 0
  ) {
      (Object.hasOwn(options, "allowedKeys") &&
        Array.isArray(options.allowedKeys) &&
        options.allowedKeys) ||
      [],

  describe("Sanitize data with custom options as function", function () {
    describe("Sanitize simple object", function () {
      it("should sanitize dirty body.", function (done) {
        expect(sanitize({
          a: "<script>Test</script>",
          b: '<p onclick="return;">Test</p>',
          c: '<img src="/"/>',
        }, { allowedKeys: ["c"] })).to.eql({
          a: "",
          b: "<p>Test</p>",
          c: '<img src="/"/>',
        });
        done();
      });
    });

    describe("XSS bypass by using prototype pollution issue", function () {
      it("should sanitize dirty data after prototype pollution.", function (done) {
        // eslint-disable-next-line no-extend-native
        Object.prototype.allowedTags = ['script'];
        expect(sanitize({
          a: "<script>Test</script>",
        }, {})).to.eql({
          a: "",
        });
        done();
      });
    });
  });