CVE-2022-2216: Server-Side Request Forgery (SSRF) (CWE-918)
value: Server-Side Request Forgery (SSRF) in GitHub repository ionicabizau/parse-url prior to 7.0.0.
CWE description: The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.
CVSS v3 base score: 9.8
Published: 2022-06-27T12: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 || ""