CVE-2021-3647: URL Redirection to Untrusted Site ('Open Redirect') (CWE-601)
value: URI.js is vulnerable to URL Redirection to Untrusted Site
CWE description: A web application accepts a user-controlled input that specifies a link to an external site, and uses that link in a Redirect. This simplifies phishing attacks.
CVSS v3 base score: 6.1
Published: 2021-07-16T11:15Z
Repository: https://github.com/medialize/URI.js
Language: JavaScript
Files changed: src/URI.js, test/urls.js
Commit date: 2021-07-11 20:27:51 +0200
Commit message: fix(parse): more backslash galore #410

issue reported privately by @ready-research via https://huntr.dev/

Diff: diff --git a/src/URI.js b/src/URI.js
index 0a5e77e..9a601a3 100644
--- a/src/URI.js
+++ b/src/URI.js
@@ -510,10 +510,13 @@
       // escaping?
       parts.query = string.substring(pos + 1) || null;
       string = string.substring(0, pos);
     }
 
+    // slashes and backslashes have lost all meaning for the web protocols (https, http, wss, ws)
+    string = string.replace(/^(https?|ftp|wss?)?:[/\\]*/, '$1://');
+
     // extract protocol
     if (string.substring(0, 2) === '//') {
       // relative-scheme
       parts.protocol = null;
       string = string.substring(2);
diff --git a/test/urls.js b/test/urls.js
index 30547e6..efb192f 100644
--- a/test/urls.js
+++ b/test/urls.js
@@ -2129,10 +2129,353 @@ var urls = [{
         ip4: false,
         ip6: false,
         idn: false,
         punycode: false
       }
+    }, {
+      name: 'backslashes protocol excessive',
+      url: 'https:/\/\/\attacker.com',
+      _url: 'https://attacker.com/',
+      parts: {
+        protocol: 'https',
+        username: null,
+        password: null,
+        hostname: 'attacker.com',
+        port: null,
+        path: '/',
+        query: null,
+        fragment: null
+      },
+      accessors: {
+        protocol: 'https',
+        username: '',
+        password: '',
+        port: '',
+        path: '/',
+        query: '',
+        fragment: '',
+        resource: '/',
+        authority: 'attacker.com',
+        origin: 'https://attacker.com',
+        userinfo: '',
+        subdomain: '',
+        domain: 'attacker.com',
+        tld: 'com',
+        directory: '/',
+        filename: '',
+        suffix: '',
+        hash: '',
+        search: '',
+        host: 'attacker.com',
+        hostname: 'attacker.com'
+      },
+      is: {
+        urn: false,
+        url: true,
+        relative: false,
+        name: true,
+        sld: false,
+        ip: false,
+        ip4: false,
+        ip6: false,
+        idn: false,
+        punycode: false
+      }
+    }, {
+      name: 'no slash protocol https',
+      url: 'https:attacker.com',
+      _url: 'https://attacker.com/',
+      parts: {
+        protocol: 'https',
+        username: null,
+        password: null,
+        hostname: 'attacker.com',
+        port: null,
+        path: '/',
+        query: null,
+        fragment: null
+      },
+      accessors: {
+        protocol: 'https',
+        username: '',
+        password: '',
+        port: '',
+        path: '/',
+        query: '',
+        fragment: '',
+        resource: '/',
+        authority: 'attacker.com',
+        origin: 'https://attacker.com',
+        userinfo: '',
+        subdomain: '',
+        domain: 'attacker.com',
+        tld: 'com',
+        directory: '/',
+        filename: '',
+        suffix: '',
+        hash: '',
+        search: '',
+        host: 'attacker.com',
+        hostname: 'attacker.com'
+      },
+      is: {
+        urn: false,
+        url: true,
+        relative: false,
+        name: true,
+        sld: false,
+        ip: false,
+        ip4: false,
+        ip6: false,
+        idn: false,
+        punycode: false
+      }
+    }, {
+      name: 'single slash protocol https',
+      url: 'https:/attacker.com',
+      _url: 'https://attacker.com/',
+      parts: {
+        protocol: 'https',
+        username: null,
+        password: null,
+        hostname: 'attacker.com',
+        port: null,
+        path: '/',
+        query: null,
+        fragment: null
+      },
+      accessors: {
+        protocol: 'https',
+        username: '',
+        password: '',
+        port: '',
+        path: '/',
+        query: '',
+        fragment: '',
+        resource: '/',
+        authority: 'attacker.com',
+        origin: 'https://attacker.com',
+        userinfo: '',
+        subdomain: '',
+        domain: 'attacker.com',
+        tld: 'com',
+        directory: '/',
+        filename: '',
+        suffix: '',
+        hash: '',
+        search: '',
+        host: 'attacker.com',
+        hostname: 'attacker.com'
+      },
+      is: {
+        urn: false,
+        url: true,
+        relative: false,
+        name: true,
+        sld: false,
+        ip: false,
+        ip4: false,
+        ip6: false,
+        idn: false,
+        punycode: false
+      }
+    }, {
+      name: 'excessive slash protocol https',
+      url: 'https://////attacker.com',
+      _url: 'https://attacker.com/',
+      parts: {
+        protocol: 'https',
+        username: null,
+        password: null,
+        hostname: 'attacker.com',
+        port: null,
+        path: '/',
+        query: null,
+        fragment: null
+      },
+      accessors: {
+        protocol: 'https',
+        username: '',
+        password: '',
+        port: '',
+        path: '/',
+        query: '',
+        fragment: '',
+        resource: '/',
+        authority: 'attacker.com',
+        origin: 'https://attacker.com',
+        userinfo: '',
+        subdomain: '',
+        domain: 'attacker.com',
+        tld: 'com',
+        directory: '/',
+        filename: '',
+        suffix: '',
+        hash: '',
+        search: '',
+        host: 'attacker.com',
+        hostname: 'attacker.com'
+      },
+      is: {
+        urn: false,
+        url: true,
+        relative: false,
+        name: true,
+        sld: false,
+        ip: false,
+        ip4: false,
+        ip6: false,
+        idn: false,
+        punycode: false
+      }
+    }, {
+      name: 'no slash protocol ftp',
+      url: 'ftp:attacker.com',
+      _url: 'ftp://attacker.com/',
+      parts: {
+        protocol: 'ftp',
+        username: null,
+        password: null,
+        hostname: 'attacker.com',
+        port: null,
+        path: '/',
+        query: null,
+        fragment: null
+      },
+      accessors: {
+        protocol: 'ftp',
+        username: '',
+        password: '',
+        port: '',
+        path: '/',
+        query: '',
+        fragment: '',
+        resource: '/',
+        authority: 'attacker.com',
+        origin: 'ftp://attacker.com',
+        userinfo: '',
+        subdomain: '',
+        domain: 'attacker.com',
+        tld: 'com',
+        directory: '/',
+        filename: '',
+        suffix: '',
+        hash: '',
+        search: '',
+        host: 'attacker.com',
+        hostname: 'attacker.com'
+      },
+      is: {
+        urn: false,
+        url: true,
+        relative: false,
+        name: true,
+        sld: false,
+        ip: false,
+        ip4: false,
+        ip6: false,
+        idn: false,
+        punycode: false
+      }
+    }, {
+      name: 'single slash protocol ftp',
+      url: 'ftp:/attacker.com',
+      _url: 'ftp://attacker.com/',
+      parts: {
+        protocol: 'ftp',
+        username: null,
+        password: null,
+        hostname: 'attacker.com',
+        port: null,
+        path: '/',
+        query: null,
+        fragment: null
+      },
+      accessors: {
+        protocol: 'ftp',
+        username: '',
+        password: '',
+        port: '',
+        path: '/',
+        query: '',
+        fragment: '',
+        resource: '/',
+        authority: 'attacker.com',
+        origin: 'ftp://attacker.com',
+        userinfo: '',
+        subdomain: '',
+        domain: 'attacker.com',
+        tld: 'com',
+        directory: '/',
+        filename: '',
+        suffix: '',
+        hash: '',
+        search: '',
+        host: 'attacker.com',
+        hostname: 'attacker.com'
+      },
+      is: {
+        urn: false,
+        url: true,
+        relative: false,
+        name: true,
+        sld: false,
+        ip: false,
+        ip4: false,
+        ip6: false,
+        idn: false,
+        punycode: false
+      }
+    }, {
+      name: 'excessive slash protocol ftp',
+      url: 'ftp://////attacker.com',
+      _url: 'ftp://attacker.com/',
+      parts: {
+        protocol: 'ftp',
+        username: null,
+        password: null,
+        hostname: 'attacker.com',
+        port: null,
+        path: '/',
+        query: null,
+        fragment: null
+      },
+      accessors: {
+        protocol: 'ftp',
+        username: '',
+        password: '',
+        port: '',
+        path: '/',
+        query: '',
+        fragment: '',
+        resource: '/',
+        authority: 'attacker.com',
+        origin: 'ftp://attacker.com',
+        userinfo: '',
+        subdomain: '',
+        domain: 'attacker.com',
+        tld: 'com',
+        directory: '/',
+        filename: '',
+        suffix: '',
+        hash: '',
+        search: '',
+        host: 'attacker.com',
+        hostname: 'attacker.com'
+      },
+      is: {
+        urn: false,
+        url: true,
+        relative: false,
+        name: true,
+        sld: false,
+        ip: false,
+        ip4: false,
+        ip6: false,
+        idn: false,
+        punycode: false
+      }
     }, {
       name: '__proto__ in query',
       url: 'http://www.example.org/?__proto__=hasOwnProperty&__proto__=eviltwin&uuid',
       parts: {
         protocol: 'http',

Fixed code:     // slashes and backslashes have lost all meaning for the web protocols (https, http, wss, ws)
    string = string.replace(/^(https?|ftp|wss?)?:[/\\]*/, '$1://');

    }, {
      name: 'backslashes protocol excessive',
      url: 'https:/\/\/\attacker.com',
      _url: 'https://attacker.com/',
      parts: {
        protocol: 'https',
        username: null,
        password: null,
        hostname: 'attacker.com',
        port: null,
        path: '/',
        query: null,
        fragment: null
      },
      accessors: {
        protocol: 'https',
        username: '',
        password: '',
        port: '',
        path: '/',
        query: '',
        fragment: '',
        resource: '/',
        authority: 'attacker.com',
        origin: 'https://attacker.com',
        userinfo: '',
        subdomain: '',
        domain: 'attacker.com',
        tld: 'com',
        directory: '/',
        filename: '',
        suffix: '',
        hash: '',
        search: '',
        host: 'attacker.com',
        hostname: 'attacker.com'
      },
      is: {
        urn: false,
        url: true,
        relative: false,
        name: true,
        sld: false,
        ip: false,
        ip4: false,
        ip6: false,
        idn: false,
        punycode: false
      }
    }, {
      name: 'no slash protocol https',
      url: 'https:attacker.com',
      _url: 'https://attacker.com/',
      parts: {
        protocol: 'https',
        username: null,
        password: null,
        hostname: 'attacker.com',
        port: null,
        path: '/',
        query: null,
        fragment: null
      },
      accessors: {
        protocol: 'https',
        username: '',
        password: '',
        port: '',
        path: '/',
        query: '',
        fragment: '',
        resource: '/',
        authority: 'attacker.com',
        origin: 'https://attacker.com',
        userinfo: '',
        subdomain: '',
        domain: 'attacker.com',
        tld: 'com',
        directory: '/',
        filename: '',
        suffix: '',
        hash: '',
        search: '',
        host: 'attacker.com',
        hostname: 'attacker.com'
      },
      is: {
        urn: false,
        url: true,
        relative: false,
        name: true,
        sld: false,
        ip: false,
        ip4: false,
        ip6: false,
        idn: false,
        punycode: false
      }
    }, {
      name: 'single slash protocol https',
      url: 'https:/attacker.com',
      _url: 'https://attacker.com/',
      parts: {
        protocol: 'https',
        username: null,
        password: null,
        hostname: 'attacker.com',
        port: null,
        path: '/',
        query: null,
        fragment: null
      },
      accessors: {
        protocol: 'https',
        username: '',
        password: '',
        port: '',
        path: '/',
        query: '',
        fragment: '',
        resource: '/',
        authority: 'attacker.com',
        origin: 'https://attacker.com',
        userinfo: '',
        subdomain: '',
        domain: 'attacker.com',
        tld: 'com',
        directory: '/',
        filename: '',
        suffix: '',
        hash: '',
        search: '',
        host: 'attacker.com',
        hostname: 'attacker.com'
      },
      is: {
        urn: false,
        url: true,
        relative: false,
        name: true,
        sld: false,
        ip: false,
        ip4: false,
        ip6: false,
        idn: false,
        punycode: false
      }
    }, {
      name: 'excessive slash protocol https',
      url: 'https://////attacker.com',
      _url: 'https://attacker.com/',
      parts: {
        protocol: 'https',
        username: null,
        password: null,
        hostname: 'attacker.com',
        port: null,
        path: '/',
        query: null,
        fragment: null
      },
      accessors: {
        protocol: 'https',
        username: '',
        password: '',
        port: '',
        path: '/',
        query: '',
        fragment: '',
        resource: '/',
        authority: 'attacker.com',
        origin: 'https://attacker.com',
        userinfo: '',
        subdomain: '',
        domain: 'attacker.com',
        tld: 'com',
        directory: '/',
        filename: '',
        suffix: '',
        hash: '',
        search: '',
        host: 'attacker.com',
        hostname: 'attacker.com'
      },
      is: {
        urn: false,
        url: true,
        relative: false,
        name: true,
        sld: false,
        ip: false,
        ip4: false,
        ip6: false,
        idn: false,
        punycode: false
      }
    }, {
      name: 'no slash protocol ftp',
      url: 'ftp:attacker.com',
      _url: 'ftp://attacker.com/',
      parts: {
        protocol: 'ftp',
        username: null,
        password: null,
        hostname: 'attacker.com',
        port: null,
        path: '/',
        query: null,
        fragment: null
      },
      accessors: {
        protocol: 'ftp',
        username: '',
        password: '',
        port: '',
        path: '/',
        query: '',
        fragment: '',
        resource: '/',
        authority: 'attacker.com',
        origin: 'ftp://attacker.com',
        userinfo: '',
        subdomain: '',
        domain: 'attacker.com',
        tld: 'com',
        directory: '/',
        filename: '',
        suffix: '',
        hash: '',
        search: '',
        host: 'attacker.com',
        hostname: 'attacker.com'
      },
      is: {
        urn: false,
        url: true,
        relative: false,
        name: true,
        sld: false,
        ip: false,
        ip4: false,
        ip6: false,
        idn: false,
        punycode: false
      }
    }, {
      name: 'single slash protocol ftp',
      url: 'ftp:/attacker.com',
      _url: 'ftp://attacker.com/',
      parts: {
        protocol: 'ftp',
        username: null,
        password: null,
        hostname: 'attacker.com',
        port: null,
        path: '/',
        query: null,
        fragment: null
      },
      accessors: {
        protocol: 'ftp',
        username: '',
        password: '',
        port: '',
        path: '/',
        query: '',
        fragment: '',
        resource: '/',
        authority: 'attacker.com',
        origin: 'ftp://attacker.com',
        userinfo: '',
        subdomain: '',
        domain: 'attacker.com',
        tld: 'com',
        directory: '/',
        filename: '',
        suffix: '',
        hash: '',
        search: '',
        host: 'attacker.com',
        hostname: 'attacker.com'
      },
      is: {
        urn: false,
        url: true,
        relative: false,
        name: true,
        sld: false,
        ip: false,
        ip4: false,
        ip6: false,
        idn: false,
        punycode: false
      }
    }, {
      name: 'excessive slash protocol ftp',
      url: 'ftp://////attacker.com',
      _url: 'ftp://attacker.com/',
      parts: {
        protocol: 'ftp',
        username: null,
        password: null,
        hostname: 'attacker.com',
        port: null,
        path: '/',
        query: null,
        fragment: null
      },
      accessors: {
        protocol: 'ftp',
        username: '',
        password: '',
        port: '',
        path: '/',
        query: '',
        fragment: '',
        resource: '/',
        authority: 'attacker.com',
        origin: 'ftp://attacker.com',
        userinfo: '',
        subdomain: '',
        domain: 'attacker.com',
        tld: 'com',
        directory: '/',
        filename: '',
        suffix: '',
        hash: '',
        search: '',
        host: 'attacker.com',
        hostname: 'attacker.com'
      },
      is: {
        urn: false,
        url: true,
        relative: false,
        name: true,
        sld: false,
        ip: false,
        ip4: false,
        ip6: false,
        idn: false,
        punycode: false
      }