CVE-2022-2218: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: Cross-site Scripting (XSS) - Stored in GitHub repository ionicabizau/parse-url prior to 7.0.0.
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: 2022-06-27T13:15Z
Repository: https://github.com/ionicabizau/parse-url
Language: JavaScript
Files changed: lib/index.js, package.json, test/index.js
Commit date: 2022-06-27 12:19:36 +0300
Commit message: Refactor codebase, upgrade dependencies

Diff: diff --git a/lib/index.js b/lib/index.js
index 31b2a04..40966e9 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -1,51 +1,72 @@
 "use strict"
 
+// Dependencies
 const parsePath = require("parse-path")
     , normalizeUrl = require("normalize-url")
 
+
 /**
  * parseUrl
  * Parses the input url.
  *
  * **Note**: This *throws* if invalid urls are provided.
  *
  * @name parseUrl
  * @function
  * @param {String} url The input url.
- * @param {Boolean|Object} normalize Wheter to normalize the url or not.
+ * @param {Boolean|Object} normalize Whether to normalize the url or not.
  *                         Default is `false`. If `true`, the url will
  *                         be normalized. If an object, it will be the
  *                         options object sent to [`normalize-url`](https://github.com/sindresorhus/normalize-url).
  *
  *                         For SSH urls, normalize won't work.
  *
  * @return {Object} An object containing the following fields:
  *
- *  - `protocols` (Array): An array with the url protocols (usually it has one element).
- *  - `protocol` (String): The first protocol, `"ssh"` (if the url is a ssh url) or `"file"`.
- *  - `port` (null|Number): The domain port.
- *  - `resource` (String): The url domain (including subdomains).
- *  - `user` (String): The authentication user (usually for ssh urls).
- *  - `pathname` (String): The url pathname.
- *  - `hash` (String): The url hash.
- *  - `search` (String): The url querystring value.
- *  - `href` (String): The input url.
- *  - `query` (Object): The url querystring, parsed as object.
+ *    - `protocols` (Array): An array with the url protocols (usually it has one element).
+ *    - `protocol` (String): The first protocol, `"ssh"` (if the url is a ssh url) or `"file"`.
+ *    - `port` (null|Number): The domain port.
+ *    - `resource` (String): The url domain (including subdomains).
+ *    - `user` (String): The authentication user (usually for ssh urls).
+ *    - `pathname` (String): The url pathname.
+ *    - `hash` (String): The url hash.
+ *    - `search` (String): The url querystring value.
+ *    - `href` (String): The input url.
+ *    - `query` (Object): The url querystring, parsed as object.
  */
-function parseUrl(url, normalize = false) {
+const parseUrl = (url, normalize = false) => {
+
+    // Constants
+    const GIT_RE = /((git@|http(s)?:\/\/)([\w\.@]+)(\/|:))(([\~,\w,\-,\_,\/]+)(.git){0,1}((\/){0,1}))/
+
     if (typeof url !== "string" || !url.trim()) {
         throw new Error("Invalid url.")
     }
+
     if (normalize) {
         if (typeof normalize !== "object") {
             normalize = {
                 stripHash: false
             }
         }
         url = normalizeUrl(url, normalize)
     }
+
     const parsed = parsePath(url)
+
+    // Potential git-ssh urls
+    if (parsed.protocol === "file") {
+        const matched  = parsed.href.match(GIT_RE)
+        if (matched) {
+            parsed.protocols = ["ssh"]
+            parsed.protocol = "ssh"
+            parsed.resource = matched[4]
+            parsed.user = "git"
+            parsed.pathname = `/${matched[6]}`
+        }
+    }
+
     return parsed;
 }
 
 module.exports = parseUrl;
diff --git a/package.json b/package.json
index 855044a..1ff55c1 100644
--- a/package.json
+++ b/package.json
@@ -29,14 +29,14 @@
   "homepage": "https://github.com/IonicaBizau/parse-url",
   "devDependencies": {
     "tester": "^1.3.1"
   },
   "dependencies": {
-    "is-ssh": "^1.3.0",
+    "is-ssh": "^1.4.0",
     "normalize-url": "^6.1.0",
-    "parse-path": "^4.0.4",
-    "protocols": "^1.4.0"
+    "parse-path": "^5.0.0",
+    "protocols": "^2.0.1"
   },
   "files": [
     "bin/",
     "app/",
     "lib/",
@@ -54,6 +54,6 @@
   "blah": {
     "description": [
       "For low-level path parsing, check out [`parse-path`](https://github.com/IonicaBizau/parse-path). This very module is designed to parse urls. By default the urls are normalized."
     ]
   }
-}
\ No newline at end of file
+}
diff --git a/test/index.js b/test/index.js
index 339af1f..e16affe 100644
--- a/test/index.js
+++ b/test/index.js
@@ -1,105 +1,112 @@
 // Dependencies
 const parseUrl = require("../lib")
     , tester = require("tester")
     , normalizeUrl = require("normalize-url")
-    , qs = require("querystring")
     ;
 
 const INPUTS = [
     [
         "http://ionicabizau.net/blog"
       , {
             protocols: [ "http" ]
           , protocol: "http"
-          , port: null
+          , port: ""
           , resource: "ionicabizau.net"
           , user: ""
           , pathname: "/blog"
           , hash: ""
           , search: ""
+          , query: {}
         }
     ]
   , [
         "//ionicabizau.net/foo.js"
       , {
             protocols: ["http"]
           , protocol: "http"
-          , port: null
+          , port: ""
           , resource: "ionicabizau.net"
           , user: ""
           , pathname: "/foo.js"
           , hash: ""
           , search: ""
+          , query: {}
         }
     ]
   , [
         "http://domain.com/path/name#some-hash?foo=bar"
       , {
             protocols: ["http"]
           , protocol: "http"
-          , port: null
+          , port: ""
           , resource: "domain.com"
           , user: ""
           , pathname: "/path/name"
           , hash: "some-hash?foo=bar"
           , search: ""
+          , query: {}
         }
     ]
   , [
         ["git+ssh://git@host.xz/path/name.git", false]
       , {
             protocols: ["git", "ssh"]
           , protocol: "git"
-          , port: null
+          , port: ""
           , resource: "host.xz"
           , user: "git"
           , pathname: "/path/name.git"
           , hash: ""
           , search: ""
+          , query: {}
         }
     ]
   , [
         ["git@github.com:IonicaBizau/git-stats.git", false]
       , {
-            protocols: []
+            protocols: ["ssh"]
           , protocol: "ssh"
-          , port: null
+          , port: ""
           , resource: "github.com"
           , user: "git"
           , pathname: "/IonicaBizau/git-stats.git"
           , hash: ""
           , search: ""
+          , query: {}
         }
     ]
   , [
         ["http://ionicabizau.net/with-true-normalize", true]
       , {
             protocols: [ "http" ]
           , protocol: "http"
-          , port: null
+          , port: ""
           , resource: "ionicabizau.net"
           , user: ""
           , pathname: "/with-true-normalize"
           , hash: ""
           , search: ""
+          , query: {}
         }
     ]
 ];
 
 tester.describe("check urls", test => {
     INPUTS.forEach(function (c) {
         let url = Array.isArray(c[0]) ? c[0][0] : c[0]
         test.should("support " + url, () => {
             const res = parseUrl(url, c[0][1] !== false);
+
             if (c[0][1] !== false) {
                 url = normalizeUrl(url, {
                     stripHash: false
                 })
             }
-            c[1].query = qs.parse(c[1].search)
-            c[1].href = url
+
+            c[1].href = c[1].href || url
+            c[1].password = c[1].password || ""
             test.expect(res).toEqual(c[1]);
         });
     });
 
     test.should("throw if url is empty", () => {

Vulnerable code:  * @param {Boolean|Object} normalize Wheter to normalize the url or not.
 *  - `protocols` (Array): An array with the url protocols (usually it has one element).
 *  - `protocol` (String): The first protocol, `"ssh"` (if the url is a ssh url) or `"file"`.
 *  - `port` (null|Number): The domain port.
 *  - `resource` (String): The url domain (including subdomains).
 *  - `user` (String): The authentication user (usually for ssh urls).
 *  - `pathname` (String): The url pathname.
 *  - `hash` (String): The url hash.
 *  - `search` (String): The url querystring value.
 *  - `href` (String): The input url.
 *  - `query` (Object): The url querystring, parsed as object.
function parseUrl(url, normalize = false) {
    "is-ssh": "^1.3.0",
    "parse-path": "^4.0.4",
    "protocols": "^1.4.0"
}
    , qs = require("querystring")
          , port: null
          , port: null
          , port: null
          , port: null
            protocols: []
          , port: null
          , port: null
            c[1].query = qs.parse(c[1].search)
            c[1].href = url
Fixed code: // Dependencies

 * @param {Boolean|Object} normalize Whether to normalize the url or not.
 *    - `protocols` (Array): An array with the url protocols (usually it has one element).
 *    - `protocol` (String): The first protocol, `"ssh"` (if the url is a ssh url) or `"file"`.
 *    - `port` (null|Number): The domain port.
 *    - `resource` (String): The url domain (including subdomains).
 *    - `user` (String): The authentication user (usually for ssh urls).
 *    - `pathname` (String): The url pathname.
 *    - `hash` (String): The url hash.
 *    - `search` (String): The url querystring value.
 *    - `href` (String): The input url.
 *    - `query` (Object): The url querystring, parsed as object.
const parseUrl = (url, normalize = false) => {

    // Constants
    const GIT_RE = /((git@|http(s)?:\/\/)([\w\.@]+)(\/|:))(([\~,\w,\-,\_,\/]+)(.git){0,1}((\/){0,1}))/




    // Potential git-ssh urls
    if (parsed.protocol === "file") {
        const matched  = parsed.href.match(GIT_RE)
        if (matched) {
            parsed.protocols = ["ssh"]
            parsed.protocol = "ssh"
            parsed.resource = matched[4]
            parsed.user = "git"
            parsed.pathname = `/${matched[6]}`
        }
    }

    "is-ssh": "^1.4.0",
    "parse-path": "^5.0.0",
    "protocols": "^2.0.1"
}
          , port: ""
          , query: {}
          , port: ""
          , query: {}
          , port: ""
          , query: {}
          , port: ""
          , query: {}
            protocols: ["ssh"]
          , port: ""
          , query: {}
          , port: ""
          , query: {}


            c[1].href = c[1].href || url
            c[1].password = c[1].password || ""