CVE-2022-2900: Server-Side Request Forgery (SSRF) (CWE-918)
value: Server-Side Request Forgery (SSRF) in GitHub repository ionicabizau/parse-url prior to 8.1.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.1
Published: 2022-09-14T11:15Z
Repository: https://github.com/ionicabizau/parse-url
Language: JavaScript
Files changed: lib/index.js, test/index.js
Commit date: 2022-08-03 11:22:27 +0300
Commit message: Throw if url is invalid. Add a length limit.

Diff: diff --git a/lib/index.js b/lib/index.js
index b806431..fe5a052 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -30,22 +30,31 @@ import normalizeUrl from "normalize-url";
  *    - `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.
+ *    - `parse_failed` (Boolean): Whether the parsing failed or not.
  */
 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()) {
-        const err = new Error("Invalid url.")
+    const throwErr = msg => {
+        const err = new Error(msg)
         err.subject_url = url
         throw err
     }
 
+    if (typeof url !== "string" || !url.trim()) {
+        throwErr("Invalid url.")
+    }
+
+    if (url.length > parseUrl.MAX_INPUT_LENGTH) {
+        throwErr("Input exceeds maximum length. If needed, change the value of parseUrl.MAX_INPUT_LENGTH.")
+    }
+
     if (normalize) {
         if (typeof normalize !== "object") {
             normalize = {
                 stripHash: false
             }
@@ -54,21 +63,26 @@ const parseUrl = (url, normalize = false) => {
     }
 
     const parsed = parsePath(url)
 
     // Potential git-ssh urls
-    if (parsed.protocol === "file") {
+    if (parsed.parse_failed) {
         const matched  = parsed.href.match(GIT_RE)
         if (matched) {
             parsed.protocols = ["ssh"]
             parsed.protocol = "ssh"
             parsed.resource = matched[4]
             parsed.host = matched[4]
             parsed.user = "git"
             parsed.pathname = `/${matched[6]}`
+            parsed.parse_failed = false
+        } else {
+            throwErr("URL parsing failed.")
         }
     }
 
     return parsed;
 }
 
+parseUrl.MAX_INPUT_LENGTH = 2048
+
 export default parseUrl;
diff --git a/test/index.js b/test/index.js
index 1a50435..288c36c 100644
--- a/test/index.js
+++ b/test/index.js
@@ -15,10 +15,11 @@ const INPUTS = [
           , user: ""
           , pathname: "/blog"
           , hash: ""
           , search: ""
           , query: {}
+          , parse_failed: false
         }
     ]
   , [
         "//ionicabizau.net/foo.js"
       , {
@@ -30,10 +31,11 @@ const INPUTS = [
           , user: ""
           , pathname: "/foo.js"
           , hash: ""
           , search: ""
           , query: {}
+          , parse_failed: false
         }
     ]
   , [
         "http://domain.com/path/name#some-hash?foo=bar"
       , {
@@ -45,10 +47,11 @@ const INPUTS = [
           , user: ""
           , pathname: "/path/name"
           , hash: "some-hash?foo=bar"
           , search: ""
           , query: {}
+          , parse_failed: false
         }
     ]
   , [
         ["git+ssh://git@host.xz/path/name.git", false]
       , {
@@ -60,10 +63,11 @@ const INPUTS = [
           , user: "git"
           , pathname: "/path/name.git"
           , hash: ""
           , search: ""
           , query: {}
+          , parse_failed: false
         }
     ]
   , [
         ["git@github.com:IonicaBizau/git-stats.git", false]
       , {
@@ -75,10 +79,11 @@ const INPUTS = [
           , user: "git"
           , pathname: "/IonicaBizau/git-stats.git"
           , hash: ""
           , search: ""
           , query: {}
+          , parse_failed: false
         }
     ]
   , [
         ["http://ionicabizau.net/with-true-normalize", true]
       , {
@@ -90,10 +95,11 @@ const INPUTS = [
           , user: ""
           , pathname: "/with-true-normalize"
           , hash: ""
           , search: ""
           , query: {}
+          , parse_failed: false
         }
     ]
   , [
         ["file:///etc/passwd?#http://a:1:1", false]
       , {
@@ -105,26 +111,28 @@ const INPUTS = [
           , user: ""
           , pathname: "/etc/passwd"
           , hash: "http://a:1:1"
           , search: ""
           , query: {}
+          , parse_failed: false
         }
     ]
   , [
         ["git@github.my-enterprise.com:my-org/my-repo.git", false],
         {
             protocols: [ 'ssh' ]
-            , protocol: 'ssh'
-            , port: ''
-            , resource: 'github.my-enterprise.com'
-            , host: 'github.my-enterprise.com'
-            , user: 'git'
-            , password: ''
-            , pathname: '/my-org/my-repo.git'
-            , hash: ''
-            , search: ''
-            , query: {}
+          , protocol: 'ssh'
+          , port: ''
+          , resource: 'github.my-enterprise.com'
+          , host: 'github.my-enterprise.com'
+          , user: 'git'
+          , password: ''
+          , pathname: '/my-org/my-repo.git'
+          , hash: ''
+          , search: ''
+          , query: {}
+          , parse_failed: false
         }
     ]
   , [
       ["git@github.com:halup/Cloud.API.Gateway.git", false]
     , {
@@ -136,10 +144,11 @@ const INPUTS = [
         , user: "git"
         , pathname: "/halup/Cloud.API.Gateway.git"
         , hash: ""
         , search: ""
         , query: {}
+        , parse_failed: false
       }
   ]
 ];
 
 tester.describe("check urls", test => {
@@ -163,6 +172,19 @@ tester.describe("check urls", test => {
     test.should("throw if url is empty", () => {
         test.expect(() => {
             parseUrl("")
         }).toThrow(/invalid url/i)
     })
+
+    test.should("throw if url is too long", () => {
+        parseUrl.MAX_INPUT_LENGTH = 10
+        test.expect(() => {
+            parseUrl("https://domain.com/")
+        }).toThrow(/input exceeds maximum length/i)
+    })
+
+    test.should("throw if url is invalid", () => {
+        test.expect(() => {
+            parseUrl("foo")
+        }).toThrow(/url parsing failed/i)
+    })
 });

Vulnerable code:     if (typeof url !== "string" || !url.trim()) {
        const err = new Error("Invalid url.")
    if (parsed.protocol === "file") {
            , protocol: 'ssh'
            , port: ''
            , resource: 'github.my-enterprise.com'
            , host: 'github.my-enterprise.com'
            , user: 'git'
            , password: ''
            , pathname: '/my-org/my-repo.git'
            , hash: ''
            , search: ''
            , query: {}
Fixed code:  *    - `parse_failed` (Boolean): Whether the parsing failed or not.
    const throwErr = msg => {
        const err = new Error(msg)
    if (typeof url !== "string" || !url.trim()) {
        throwErr("Invalid url.")
    }

    if (url.length > parseUrl.MAX_INPUT_LENGTH) {
        throwErr("Input exceeds maximum length. If needed, change the value of parseUrl.MAX_INPUT_LENGTH.")
    }

    if (parsed.parse_failed) {
            parsed.parse_failed = false
        } else {
            throwErr("URL parsing failed.")
parseUrl.MAX_INPUT_LENGTH = 2048

          , parse_failed: false
          , parse_failed: false
          , parse_failed: false
          , parse_failed: false
          , parse_failed: false
          , parse_failed: false
          , parse_failed: false
          , protocol: 'ssh'
          , port: ''
          , resource: 'github.my-enterprise.com'
          , host: 'github.my-enterprise.com'
          , user: 'git'
          , password: ''
          , pathname: '/my-org/my-repo.git'
          , hash: ''
          , search: ''
          , query: {}
          , parse_failed: false
        , parse_failed: false

    test.should("throw if url is too long", () => {
        parseUrl.MAX_INPUT_LENGTH = 10
        test.expect(() => {
            parseUrl("https://domain.com/")
        }).toThrow(/input exceeds maximum length/i)
    })

    test.should("throw if url is invalid", () => {
        test.expect(() => {
            parseUrl("foo")
        }).toThrow(/url parsing failed/i)
    })