CVE-2021-23342: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: This affects the package docsify before 4.12.0. It is possible to bypass the remediation done by CVE-2020-7680 and execute malicious JavaScript through the following methods 1) When parsing HTML from remote URLs, the HTML code on the main page is sanitized, but this sanitization is not taking place in the sidebar. 2) The isURL external check can be bypassed by inserting more “////” characters
CWE description: The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
CVSS v3 base score: 6.1
Published: 2021-02-19T17:15Z
Repository: https://github.com/docsifyjs/docsify
Language: JavaScript
Files changed: src/core/fetch/index.js, test/e2e/security.test.js
Commit date: 2021-02-18 20:06:44 +0800
Commit message: fix: isExternal check with malformed URL + tests (#1510)

Fix #1477. Fix #1126. Follow-up to #1489.
Diff: diff --git a/src/core/fetch/index.js b/src/core/fetch/index.js
index 0fac23b..2d6f813 100644
--- a/src/core/fetch/index.js
+++ b/src/core/fetch/index.js
@@ -20,11 +20,11 @@ function loadNested(path, qs, file, next, vm, first) {
   ).then(next, _ => loadNested(path, qs, file, next, vm));
 }
 
 function isExternal(url) {
   let match = url.match(
-    /^([^:/?#]+:)?(?:\/\/([^/?#]*))?([^?#]+)?(\?[^#]*)?(#.*)?/
+    /^([^:/?#]+:)?(?:\/{2,}([^/?#]*))?([^?#]+)?(\?[^#]*)?(#.*)?/
   );
   if (
     typeof match[1] === 'string' &&
     match[1].length > 0 &&
     match[1].toLowerCase() !== location.protocol
diff --git a/test/e2e/security.test.js b/test/e2e/security.test.js
new file mode 100644
index 0000000..caffdda
--- /dev/null
+++ b/test/e2e/security.test.js
@@ -0,0 +1,32 @@
+const docsifyInit = require('../helpers/docsify-init');
+
+describe(`Security`, function() {
+  const sharedOptions = {
+    markdown: {
+      homepage: '# Hello World',
+    },
+    routes: {
+      'test.md': '# Test Page',
+    },
+  };
+
+  describe(`Cross Site Scripting (XSS)`, function() {
+    const slashStrings = ['//', '///'];
+
+    for (const slashString of slashStrings) {
+      const hash = `#${slashString}domain.com/file.md`;
+
+      test(`should not load remote content from hash (${hash})`, async () => {
+        await docsifyInit(sharedOptions);
+        await expect(page).toHaveText('#main', 'Hello World');
+        await page.evaluate(() => (location.hash = '#/test'));
+        await expect(page).toHaveText('#main', 'Test Page');
+        await page.evaluate(newHash => {
+          location.hash = newHash;
+        }, hash);
+        await expect(page).toHaveText('#main', 'Hello World');
+        expect(page.url()).toMatch(/#\/$/);
+      });
+    }
+  });
+});

Vulnerable code:     /^([^:/?#]+:)?(?:\/\/([^/?#]*))?([^?#]+)?(\?[^#]*)?(#.*)?/
Fixed code:     /^([^:/?#]+:)?(?:\/{2,}([^/?#]*))?([^?#]+)?(\?[^#]*)?(#.*)?/
const docsifyInit = require('../helpers/docsify-init');

describe(`Security`, function() {
  const sharedOptions = {
    markdown: {
      homepage: '# Hello World',
    },
    routes: {
      'test.md': '# Test Page',
    },
  };

  describe(`Cross Site Scripting (XSS)`, function() {
    const slashStrings = ['//', '///'];

    for (const slashString of slashStrings) {
      const hash = `#${slashString}domain.com/file.md`;

      test(`should not load remote content from hash (${hash})`, async () => {
        await docsifyInit(sharedOptions);
        await expect(page).toHaveText('#main', 'Hello World');
        await page.evaluate(() => (location.hash = '#/test'));
        await expect(page).toHaveText('#main', 'Test Page');
        await page.evaluate(newHash => {
          location.hash = newHash;
        }, hash);
        await expect(page).toHaveText('#main', 'Hello World');
        expect(page.url()).toMatch(/#\/$/);
      });
    }
  });
});