CVE-2022-0624: Authorization Bypass Through User-Controlled Key (CWE-639)
value: Authorization Bypass Through User-Controlled Key in GitHub repository ionicabizau/parse-path prior to 5.0.0.
CWE description: The system's authorization functionality does not prevent one user from gaining access to another user's data or record by modifying the key value identifying the data.
CVSS v3 base score: 7.3
Published: 2022-06-28T09:15Z
Repository: https://github.com/ionicabizau/parse-path
Language: JavaScript
Files changed: lib/index.js, package.json, test/index.js
Commit date: 2022-06-27 11:35:20 +0300
Commit message: Refactor the codebase to make it more secure

Diff: diff --git a/lib/index.js b/lib/index.js
index 16ffe4d..2da330b 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -1,10 +1,6 @@
-// Dependencies
 const protocols = require("protocols")
-    , isSsh = require("is-ssh")
-    , qs = require("query-string")
-    ;
 
 /**
  * parsePath
  * Parses the input url.
  *
@@ -23,113 +19,53 @@ const protocols = require("protocols")
  *  - `search` (String): The url querystring value.
  *  - `href` (String): The input url.
  *  - `query` (Object): The url querystring, parsed as object.
  */
 function parsePath(url) {
-    url = (url || "").trim().replace(/\r?\n|\r/gm, "")
-    var output = {
-            protocols: protocols(url)
-          , protocol: null
-          , port: null
-          , resource: ""
-          , user: ""
-          , pathname: ""
-          , hash: ""
-          , search: ""
-          , href: url
-          , query: Object.create(null)
-        }
-      , protocolIndex = url.indexOf("://")
-      , resourceIndex = -1
-      , splits = null
-      , parts = null
-      ;
 
-    if (url.startsWith(".")) {
-        if (url.startsWith("./")) {
-            url = url.substring(2);
-        }
-        output.pathname = url;
-        output.protocol = "file";
+    const output = {
+        protocols: []
+      , protocol: null
+      , port: null
+      , resource: ""
+      , user: ""
+      , password: ""
+      , pathname: ""
+      , hash: ""
+      , search: ""
+      , href: url
+      , query: {}
     }
 
-    const firstChar = url.charAt(1)
-    if (!output.protocol) {
+    try {
+        const parsed = new URL(url)
+        output.protocols = protocols(parsed)
         output.protocol = output.protocols[0]
-        if (!output.protocol) {
-            if (isSsh(url)) {
-                output.protocol = "ssh"
-            } else if (firstChar === "/" || firstChar === "~") {
-                url = url.substring(2)
-                output.protocol = "file"
-            } else {
-                output.protocol = "file"
-            }
-        }
-    }
-
-    if (protocolIndex !== -1) {
-        url = url.substring(protocolIndex + 3);
-    }
-
-    parts = url.split(/\/|\\/);
-    if (output.protocol !== "file") {
-        output.resource = parts.shift();
-    } else {
-        output.resource = "";
-    }
-
-    // user@domain
-    splits = output.resource.split("@");
-    if (splits.length === 2) {
-        output.user = splits[0];
-        output.resource = splits[1];
-    }
-
-
-    // domain.com:port
-    splits = output.resource.split(":");
-    if (splits.length === 2) {
-        output.resource = splits[0];
-        const port = splits[1];
-        if (port) {
-            output.port = Number(port);
-            if (isNaN(output.port) || port.match(/^\d+$/) === null) {
-                output.port = null;
-                parts.unshift(port);
-            }
-        } else {
-            output.port = null
-        }
-    }
-
-    // Remove empty elements
-    parts = parts.filter(Boolean);
-
-    // Stringify the pathname
-    if (output.protocol === "file") {
-        output.pathname = output.href
-    } else {
-      output.pathname = output.pathname || ((output.protocol !== "file" || output.href[0] === "/" ? "/" : "") + parts.join("/"));
-    }
-
-    // #some-hash
-    splits = output.pathname.split("#");
-    if (splits.length === 2) {
-        output.pathname = splits[0];
-        output.hash = splits[1];
-    }
-
-    // ?foo=bar
-    splits = output.pathname.split("?");
-    if (splits.length === 2) {
-        output.pathname = splits[0];
-        output.search = splits[1];
+        output.port = parsed.port
+        output.resource =  parsed.host
+        output.user = parsed.username || ""
+        output.password = parsed.password || ""
+        output.pathname = parsed.pathname
+        output.hash = parsed.hash.slice(1)
+        output.search = parsed.search.slice(1)
+        output.href = parsed.href
+        output.query = Object.fromEntries(parsed.searchParams)
+    } catch (e) {
+        // TODO Maybe check if it is a valid local file path
+        //      In any case, these will be parsed by higher
+        //      level parsers such as parse-url, git-url-parse, git-up
+        output.protocols = ["file"]
+        output.protocol = output.protocols[0]
+        output.port = ""
+        output.resource =  ""
+        output.user = ""
+        output.pathname = ""
+        output.hash = ""
+        output.search = ""
+        output.href = url
+        output.query = {}
     }
 
-    output.query = qs.parse(output.search);
-    output.href = output.href.replace(/\/$/, "")
-    output.pathname = output.pathname.replace(/\/$/, "")
     return output;
 }
 
 module.exports = parsePath;
diff --git a/package.json b/package.json
index 960f3b1..cff33ae 100644
--- a/package.json
+++ b/package.json
@@ -50,6 +50,6 @@
     "index.js",
     "bloggify.js",
     "bloggify.json",
     "bloggify/"
   ]
-}
\ No newline at end of file
+}
diff --git a/test/index.js b/test/index.js
index 1f98d4d..daba258 100644
--- a/test/index.js
+++ b/test/index.js
@@ -1,209 +1,240 @@
 // Dependencies
 const parseUrl = require("../lib")
     , tester = require("tester")
-    , qs = require("querystring")
     ;
 
 const INPUTS = [
     [
         "http://ionicabizau.net/blog"
       , {
             protocols: [ "http" ]
           , protocol: "http"
-          , port: null
+          , port: ""
           , resource: "ionicabizau.net"
           , user: ""
           , pathname: "/blog"
           , hash: ""
           , search: ""
+          , href: "http://ionicabizau.net/blog"
+          , query: {}
         }
     ]
   , [
         "    http://ionicabizau.net/blog   "
       , {
             protocols: [ "http" ]
           , protocol: "http"
-          , port: null
+          , port: ""
           , resource: "ionicabizau.net"
           , user: ""
           , pathname: "/blog"
           , hash: ""
           , search: ""
+          , href: "http://ionicabizau.net/blog"
+          , query: {}
         }
     ]
   , [
         "http://domain.com/path/name?foo=bar&bar=42#some-hash"
       , {
             protocols: ["http"]
           , protocol: "http"
-          , port: null
+          , port: ""
           , resource: "domain.com"
           , user: ""
           , pathname: "/path/name"
           , hash: "some-hash"
           , search: "foo=bar&bar=42"
+          , query: { foo: "bar", bar: "42" }
+          , href: "http://domain.com/path/name?foo=bar&bar=42#some-hash"
         }
     ]
   , [
         "http://domain.com/path/name#some-hash?foo=bar&bar=42"
       , {
             protocols: ["http"]
           , protocol: "http"
-          , port: null
+          , port: ""
           , resource: "domain.com"
           , user: ""
           , pathname: "/path/name"
           , hash: "some-hash?foo=bar&bar=42"
           , search: ""
+          , query: {}
+          , href: "http://domain.com/path/name#some-hash?foo=bar&bar=42"
         }
     ]
   , [
         "git+ssh://git@host.xz/path/name.git"
       , {
             protocols: ["git", "ssh"]
           , protocol: "git"
-          , port: null
+          , port: ""
           , resource: "host.xz"
           , user: "git"
           , pathname: "/path/name.git"
           , hash: ""
           , search: ""
+          , query: {}
+          , href: "git+ssh://git@host.xz/path/name.git"
         }
     ]
   , [
+        // NOTE:parse-path will look at this as a local path
+        //      For parsing it as url, please use parse-url
         "git@github.com:IonicaBizau/git-stats.git"
       , {
-            protocols: []
-          , protocol: "ssh"
-          , port: null
-          , resource: "github.com"
-          , user: "git"
-          , pathname: "/IonicaBizau/git-stats.git"
+            protocols: ["file"]
+          , protocol: "file"
+          , port: ""
+          , resource: ""
+          , user: ""
+          , pathname: ""
           , hash: ""
           , search: ""
+          , query: {}
+          , href: "git@github.com:IonicaBizau/git-stats.git"
         }
     ]
   , [
         "/path/to/file.png"
       , {
-            protocols: []
+            protocols: ["file"]
           , protocol: "file"
-          , port: null
+          , port: ""
           , resource: ""
           , user: ""
-          , pathname: "/path/to/file.png"
+          , pathname: ""
           , hash: ""
           , search: ""
+          , query: {}
+          , href: "/path/to/file.png"
         }
     ]
   , [
         "./path/to/file.png"
       , {
-            protocols: []
+            protocols: ["file"]
           , protocol: "file"
-          , port: null
+          , port: ""
           , resource: ""
           , user: ""
-          , pathname: "./path/to/file.png"
+          , pathname: ""
           , hash: ""
           , search: ""
+          , query: {}
+          , href: "./path/to/file.png"
         }
     ]
   , [
         "./.path/to/file.png"
       , {
-            protocols: []
+            protocols: ["file"]
           , protocol: "file"
-          , port: null
+          , port: ""
           , resource: ""
           , user: ""
-          , pathname: "./.path/to/file.png"
+          , pathname: ""
           , hash: ""
           , search: ""
+          , query: {}
+          , href: "./.path/to/file.png"
         }
     ]
   , [
         ".path/to/file.png"
       , {
-            protocols: []
+            protocols: ["file"]
           , protocol: "file"
-          , port: null
+          , port: ""
           , resource: ""
           , user: ""
-          , pathname: ".path/to/file.png"
+          , pathname: ""
           , hash: ""
           , search: ""
+          , query: {}
+          , href: ".path/to/file.png"
         }
     ]
   , [
         "path/to/file.png"
       , {
-            protocols: []
+            protocols: ["file"]
           , protocol: "file"
-          , port: null
+          , port: ""
           , resource: ""
           , user: ""
-          , pathname: "path/to/file.png"
+          , pathname: ""
           , hash: ""
           , search: ""
+          , query: {}
+          , href: "path/to/file.png"
         }
     ], [
       "git@github.com:9IonicaBizau/git-stats.git"
     , {
-          protocols: []
-        , protocol: "ssh"
-        , port: null
-        , resource: "github.com"
-        , user: "git"
-        , pathname: "/9IonicaBizau/git-stats.git"
+          protocols: ["file"]
+        , protocol: "file"
+        , port: ""
+        , resource: ""
+        , user: ""
+        , pathname: ""
         , hash: ""
         , search: ""
+        , query: {}
+        , href: "git@github.com:9IonicaBizau/git-stats.git"
       }
     ], [
       "git@github.com:0xABC/git-stats.git"
     , {
-          protocols: []
-        , protocol: "ssh"
-        , port: null
-        , resource: "github.com"
-        , user: "git"
-        , pathname: "/0xABC/git-stats.git"
+          protocols: ["file"]
+        , protocol: "file"
+        , port: ""
+        , resource: ""
+        , user: ""
+        , pathname: ""
         , hash: ""
         , search: ""
+        , query: {}
+        , href: "git@github.com:0xABC/git-stats.git"
       }
     ], [
       "https://attacker.com\\@example.com"
     , {
           protocols: ["https"]
         , protocol: "https"
-        , port: null
+        , port: ""
         , resource: "attacker.com"
         , user: ""
         , pathname: "/@example.com"
         , hash: ""
         , search: ""
+        , href: "https://attacker.com/@example.com"
+        , query: {}
       }
   ], [
       "jav\r\nascript://%0aalert(1)"
     , {
           protocols: ["javascript"]
         , protocol: "javascript"
-        , port: null
+        , port: ""
         , resource: "%0aalert(1)"
         , user: ""
         , pathname: ""
         , hash: ""
+        , href: "javascript://%0aalert(1)"
+        , query: {}
         , search: ""
       }
   ]
 ];
 
 tester.describe("check urls", test => {
     INPUTS.forEach(function (c) {
         test.should("support " + c[0], () => {
-            c[1].href = c[0].trim().replace(/\r?\n|\r/gm, "");
-            c[1].query = qs.parse(c[1].search)
-            test.expect(parseUrl(c[0])).toEqual(c[1]);
+            const cParsed = parseUrl(c[0])
+            debugger
+            test.expect(cParsed).toEqual(c[1]);
         });
     });
 });

Vulnerable code: // Dependencies
    , isSsh = require("is-ssh")
    , qs = require("query-string")
    ;
    url = (url || "").trim().replace(/\r?\n|\r/gm, "")
    var output = {
            protocols: protocols(url)
          , protocol: null
          , port: null
          , resource: ""
          , user: ""
          , pathname: ""
          , hash: ""
          , search: ""
          , href: url
          , query: Object.create(null)
        }
      , protocolIndex = url.indexOf("://")
      , resourceIndex = -1
      , splits = null
      , parts = null
      ;
    if (url.startsWith(".")) {
        if (url.startsWith("./")) {
            url = url.substring(2);
        }
        output.pathname = url;
        output.protocol = "file";
    const firstChar = url.charAt(1)
    if (!output.protocol) {
        if (!output.protocol) {
            if (isSsh(url)) {
                output.protocol = "ssh"
            } else if (firstChar === "/" || firstChar === "~") {
                url = url.substring(2)
                output.protocol = "file"
            } else {
                output.protocol = "file"
            }
        }
    }

    if (protocolIndex !== -1) {
        url = url.substring(protocolIndex + 3);
    }

    parts = url.split(/\/|\\/);
    if (output.protocol !== "file") {
        output.resource = parts.shift();
    } else {
        output.resource = "";
    }

    // user@domain
    splits = output.resource.split("@");
    if (splits.length === 2) {
        output.user = splits[0];
        output.resource = splits[1];
    }


    // domain.com:port
    splits = output.resource.split(":");
    if (splits.length === 2) {
        output.resource = splits[0];
        const port = splits[1];
        if (port) {
            output.port = Number(port);
            if (isNaN(output.port) || port.match(/^\d+$/) === null) {
                output.port = null;
                parts.unshift(port);
            }
        } else {
            output.port = null
        }
    }

    // Remove empty elements
    parts = parts.filter(Boolean);

    // Stringify the pathname
    if (output.protocol === "file") {
        output.pathname = output.href
    } else {
      output.pathname = output.pathname || ((output.protocol !== "file" || output.href[0] === "/" ? "/" : "") + parts.join("/"));
    }

    // #some-hash
    splits = output.pathname.split("#");
    if (splits.length === 2) {
        output.pathname = splits[0];
        output.hash = splits[1];
    }

    // ?foo=bar
    splits = output.pathname.split("?");
    if (splits.length === 2) {
        output.pathname = splits[0];
        output.search = splits[1];
    output.query = qs.parse(output.search);
    output.href = output.href.replace(/\/$/, "")
    output.pathname = output.pathname.replace(/\/$/, "")
}
    , qs = require("querystring")
          , port: null
          , port: null
          , port: null
          , port: null
          , port: null
            protocols: []
          , protocol: "ssh"
          , port: null
          , resource: "github.com"
          , user: "git"
          , pathname: "/IonicaBizau/git-stats.git"
            protocols: []
          , port: null
          , pathname: "/path/to/file.png"
            protocols: []
          , port: null
          , pathname: "./path/to/file.png"
            protocols: []
          , port: null
          , pathname: "./.path/to/file.png"
            protocols: []
          , port: null
          , pathname: ".path/to/file.png"
            protocols: []
          , port: null
          , pathname: "path/to/file.png"
          protocols: []
        , protocol: "ssh"
        , port: null
        , resource: "github.com"
        , user: "git"
        , pathname: "/9IonicaBizau/git-stats.git"
          protocols: []
        , protocol: "ssh"
        , port: null
        , resource: "github.com"
        , user: "git"
        , pathname: "/0xABC/git-stats.git"
        , port: null
        , port: null
            c[1].href = c[0].trim().replace(/\r?\n|\r/gm, "");
            c[1].query = qs.parse(c[1].search)
            test.expect(parseUrl(c[0])).toEqual(c[1]);
Fixed code:     const output = {
        protocols: []
      , protocol: null
      , port: null
      , resource: ""
      , user: ""
      , password: ""
      , pathname: ""
      , hash: ""
      , search: ""
      , href: url
      , query: {}
    try {
        const parsed = new URL(url)
        output.protocols = protocols(parsed)
        output.port = parsed.port
        output.resource =  parsed.host
        output.user = parsed.username || ""
        output.password = parsed.password || ""
        output.pathname = parsed.pathname
        output.hash = parsed.hash.slice(1)
        output.search = parsed.search.slice(1)
        output.href = parsed.href
        output.query = Object.fromEntries(parsed.searchParams)
    } catch (e) {
        // TODO Maybe check if it is a valid local file path
        //      In any case, these will be parsed by higher
        //      level parsers such as parse-url, git-url-parse, git-up
        output.protocols = ["file"]
        output.protocol = output.protocols[0]
        output.port = ""
        output.resource =  ""
        output.user = ""
        output.pathname = ""
        output.hash = ""
        output.search = ""
        output.href = url
        output.query = {}
}
          , port: ""
          , href: "http://ionicabizau.net/blog"
          , query: {}
          , port: ""
          , href: "http://ionicabizau.net/blog"
          , query: {}
          , port: ""
          , query: { foo: "bar", bar: "42" }
          , href: "http://domain.com/path/name?foo=bar&bar=42#some-hash"
          , port: ""
          , query: {}
          , href: "http://domain.com/path/name#some-hash?foo=bar&bar=42"
          , port: ""
          , query: {}
          , href: "git+ssh://git@host.xz/path/name.git"
        // NOTE:parse-path will look at this as a local path
        //      For parsing it as url, please use parse-url
            protocols: ["file"]
          , protocol: "file"
          , port: ""
          , resource: ""
          , user: ""
          , pathname: ""
          , query: {}
          , href: "git@github.com:IonicaBizau/git-stats.git"
            protocols: ["file"]
          , port: ""
          , pathname: ""
          , query: {}
          , href: "/path/to/file.png"
            protocols: ["file"]
          , port: ""
          , pathname: ""
          , query: {}
          , href: "./path/to/file.png"
            protocols: ["file"]
          , port: ""
          , pathname: ""
          , query: {}
          , href: "./.path/to/file.png"
            protocols: ["file"]
          , port: ""
          , pathname: ""
          , query: {}
          , href: ".path/to/file.png"
            protocols: ["file"]
          , port: ""
          , pathname: ""
          , query: {}
          , href: "path/to/file.png"
          protocols: ["file"]
        , protocol: "file"
        , port: ""
        , resource: ""
        , user: ""
        , pathname: ""
        , query: {}
        , href: "git@github.com:9IonicaBizau/git-stats.git"
          protocols: ["file"]
        , protocol: "file"
        , port: ""
        , resource: ""
        , user: ""
        , pathname: ""
        , query: {}
        , href: "git@github.com:0xABC/git-stats.git"
        , port: ""
        , href: "https://attacker.com/@example.com"
        , query: {}
        , port: ""
        , href: "javascript://%0aalert(1)"
        , query: {}
            const cParsed = parseUrl(c[0])
            debugger
            test.expect(cParsed).toEqual(c[1]);