Repository: apostrophecms/sanitize-html
Pull request #460 (2021-01-26 14:00:51)

new and interesting iframe validation exploits
The WHATWG URL parser doesn't agree with browsers about these, so a regexp of our own is needed. Fortunately however they rely on behaviors regarding the protocol part of the URL which is the easiest part to clean before passing to WHATWG. Tests contributed by our friends at Checkmarx.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4861e0a..fcb4c8e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,7 +1,10 @@
 # Changelog
 
+## 2.3.2 (2021-01-26):
+- Additional fixes for iframe validation exploits. Prevent exploits based on browsers' tolerance of the use of "\" rather than "/" and the presence of whitespace at this point in the URL. Thanks to Ron Masas of [Checkmarx](https://www.checkmarx.com/) for pointing out the issue and writing unit tests.
+
 ## 2.3.1 (2021-01-22):
-- Uses the standard WHATWG URL parser to stop IDNA (Internationalized Domain Name) attacks on the iframe hostname validator. Thanks to Ron Masas of Checkmarx for pointing out the issue and suggesting the use of the WHATWG parser.
+- Uses the standard WHATWG URL parser to stop IDNA (Internationalized Domain Name) attacks on the iframe hostname validator. Thanks to Ron Masas of [Checkmarx](https://www.checkmarx.com/) for pointing out the issue and suggesting the use of the WHATWG parser.
 
 ## 2.3.0 (2020-12-16):
 - Upgrades `htmlparser2` to new major version `^6.0.0`. Thanks to [Bogdan Chadkin](https://github.com/TrySound) for the contribution.
diff --git a/index.js b/index.js
index d37ee99..fe4dd9e 100644
--- a/index.js
+++ b/index.js
@@ -304,6 +304,10 @@ function sanitizeHtml(html, options, _recursing) {
             if (name === 'iframe' && a === 'src') {
               let allowed = true;
               try {
+                // Chrome accepts \ as a substitute for / in the // at the
+                // start of a URL, so rewrite accordingly to prevent exploit.
+                // Also drop any whitespace at that point in the URL
+                value = value.replace(/^(\w+:)?\s*[\\/]\s*[\\/]/, '$1//');
                 if (value.startsWith('relative:')) {
                   // An attempt to exploit our workaround for base URLs being
                   // mandatory for relative URL validation in the WHATWG
diff --git a/package.json b/package.json
index 0d390a1..52eeca4 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "sanitize-html",
-  "version": "2.3.1",
+  "version": "2.3.2",
   "description": "Clean up user-submitted HTML, preserving whitelisted elements and whitelisted attributes on a per-element basis",
   "sideEffects": false,
   "main": "index.js",


Test patch:
diff --git a/test/test.js b/test/test.js
index 634ada4..a0ffbe2 100644
--- a/test/test.js
+++ b/test/test.js
@@ -1269,4 +1269,60 @@ describe('sanitizeHtml', function() {
       '<iframe></iframe>'
     );
   });
+  it('Should prevent hostname bypass using protocol-relative src', function () {
+    assert.strictEqual(
+      sanitizeHtml('<iframe src="/\\example.com"></iframe>', {
+        allowedTags: [ 'iframe' ],
+        allowedAttributes: {
+          iframe: [ 'src' ]
+        },
+        allowedIframeHostnames: [ 'www.youtube.com' ],
+        allowIframeRelativeUrls: true
+      }), '<iframe></iframe>'
+    );
+    assert.strictEqual(
+      sanitizeHtml('<iframe src="\\/example.com"></iframe>', {
+        allowedTags: [ 'iframe' ],
+        allowedAttributes: {
+          iframe: [ 'src' ]
+        },
+        allowedIframeHostnames: [ 'www.youtube.com' ],
+        allowIframeRelativeUrls: true
+      }), '<iframe></iframe>'
+    );
+    const linefeed = decodeURIComponent('%0A');
+    assert.strictEqual(
+      sanitizeHtml('<iframe src="/' + linefeed + '\\example.com"></iframe>', {
+        allowedTags: [ 'iframe' ],
+        allowedAttributes: {
+          iframe: [ 'src' ]
+        },
+        allowedIframeHostnames: [ 'www.youtube.com' ],
+        allowIframeRelativeUrls: true
+      }), '<iframe></iframe>'
+    );
+    const creturn = decodeURIComponent('%0D');
+    assert.strictEqual(
+      sanitizeHtml('<iframe src="/' + creturn + '\\example.com"></iframe>', {
+        allowedTags: [ 'iframe' ],
+        allowedAttributes: {
+          iframe: [ 'src' ]
+        },
+        allowedIframeHostnames: [ 'www.youtube.com' ],
+        allowIframeRelativeUrls: true
+      }), '<iframe></iframe>'
+    );
+    const tab = decodeURIComponent('%09');
+    assert.strictEqual(
+      sanitizeHtml('<iframe src="/' + tab + '\\example.com"></iframe>', {
+        allowedTags: [ 'iframe' ],
+        allowedAttributes: {
+          iframe: [ 'src' ]
+        },
+        allowedIframeHostnames: [ 'www.youtube.com' ],
+        allowIframeRelativeUrls: true
+      }), '<iframe></iframe>'
+    );
+  });
+
 });