CVE-2019-10856 (Python)

Jupyter Notebook's login redirect handling is vulnerable to an open redirect when processing the attacker-controlled next parameter. The handler attempts to allow local redirects under the configured base_url, but URL parsing can treat certain absolute-looking inputs as having an empty netloc even though browsers may interpret them as external or non-local destinations. An attacker who can cause a user to visit or submit the login flow with such a crafted next value could make the server emit a Location header that sends the user's browser away from the Notebook application, enabling phishing or other redirect-based attacks.

Weakness:
CWE-601: name: URL Redirection to Untrusted Site ('Open Redirect')
description: The web application accepts a user-controlled input that specifies a link to an external site, and uses that link in a redirect.

Repository: https://github.com/jupyter/notebook

Vulnerable code:
file_path: notebook/auth/login.py
snippet:     def _redirect_safe(self, url, default=None):
        """Redirect if url is on our PATH

        Full-domain redirects are allowed if they pass our CORS origin checks.

        Otherwise use default (self.base_url if unspecified).
        """
        if default is None:
            default = self.base_url
        # protect chrome users from mishandling unescaped backslashes.
        # \ is not valid in urls, but some browsers treat it as /
        # instead of %5C, causing `\\` to behave as `//`
        url = url.replace("\\", "%5C")
        parsed = urlparse(url)
        if parsed.netloc or not (parsed.path + '/').startswith(self.base_url):
            # require that next_url be absolute path within our path
            allow = False
            # OR pass our cross-origin check
            if parsed.netloc:
                # if full URL, run our cross-origin check:
                origin = '%s://%s' % (parsed.scheme, parsed.netloc)
                origin = origin.lower()
                if self.allow_origin:
                    allow = self.allow_origin == origin
                elif self.allow_origin_pat:
                    allow = bool(self.allow_origin_pat.match(origin))
            if not allow:
                # not allowed, use default
                self.log.warning("Not allowing login redirect to %r" % url)
                url = default
        self.redirect(url)

Fixed code:
file_path: notebook/auth/login.py
snippet:     def _redirect_safe(self, url, default=None):
        """Redirect if url is on our PATH

        Full-domain redirects are allowed if they pass our CORS origin checks.

        Otherwise use default (self.base_url if unspecified).
        """
        if default is None:
            default = self.base_url
        # protect chrome users from mishandling unescaped backslashes.
        # \ is not valid in urls, but some browsers treat it as /
        # instead of %5C, causing `\\` to behave as `//`
        url = url.replace("\\", "%5C")
        parsed = urlparse(url)
        path_only = urlunparse(parsed._replace(netloc='', scheme=''))
        if url != path_only or not (parsed.path + '/').startswith(self.base_url):
            # require that next_url be absolute path within our path
            allow = False
            # OR pass our cross-origin check
            if url != path_only:
                # if full URL, run our cross-origin check:
                origin = '%s://%s' % (parsed.scheme, parsed.netloc)
                origin = origin.lower()
                if origin == '%s://%s' % (self.request.protocol, self.request.host):
                    allow = True
                elif self.allow_origin:
                    allow = self.allow_origin == origin
                elif self.allow_origin_pat:
                    allow = bool(self.allow_origin_pat.match(origin))
            if not allow:
                # not allowed, use default
                self.log.warning("Not allowing login redirect to %r" % url)
                url = default
        self.redirect(url)