Issue 43223: [security] http.server: Open Redirection if the URL path starts with // (original) (raw)

I can only reproduce the issue if the current directory (directory used by the HTTP server, see --directory command line option) contains a .ssh/ subdirectory.

The problem is that the HTTP Header Location starts with "//domain/" and such URL is interpreted as an absolute URL of a new domain name ("domain"), rather than a relative path of the same domain ("localhost").

Maybe we should simply strip all additional leading slashes to only keep one. Replace "//path" or "/////path" with "/path" for example.


By the way, http.server uses urllib.parse.urlsplit() on the request URL without passing its own domain, and urllib.parse.urlsplit() interprets "//attacker.com/path" as if attacker.com is a host with no scheme:

urllib.parse.urlsplit('//attacker.com/path') SplitResult(scheme='', netloc='attacker.com', path='/path', query='', fragment='')

Maybe parse_qs() should be used instead? Or we should reinject the server domain and port number? I am not sure that it's an issue in practice.

SimpleHTTPRequestHandler.translate_path('//attacker.com/..%2f..%2f..%2f..%2f..%2f../.ssh') returns os.path.join(self.directory, ".ssh"). I don't think that it's an issue, it sounds like the expected behavior. We don't attempt to reject ".." in URL.


To reproduce the issue, I used two terminals.

Terminal 1:

$ python3.8 -V Python 3.8.7 $ python3.8 -m http.server Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ... 127.0.0.1 - - [15/Feb/2021 09🔞20] "GET //attacker.com/..%2f..%2f..%2f..%2f..%2f../.ssh HTTP/1.1" 301 -

Terminal 2:

$ wget 'http://127.0.0.1:8000//attacker.com/..%2f..%2f..%2f..%2f..%2f../.ssh' (...) HTTP request sent, awaiting response... 301 Moved Permanently Location: //attacker.com/..%2f..%2f..%2f..%2f..%2f../.ssh/ [following]

--2021-02-15 09🔞20-- http://attacker.com/..%2f..%2f..%2f..%2f..%2f../.ssh/ Resolving attacker.com (attacker.com)... 45.88.202.115 Connecting to attacker.com (attacker.com)|45.88.202.115|:80... connected.

(...)

wget is redirected and connects to attacker.com.

The HTTP redirection comes from Lib/http/server.py:

def send_head(self):
    path = self.translate_path(self.path)
    f = None
    if os.path.isdir(path):
        parts = urllib.parse.urlsplit(self.path)
        if not parts.path.endswith('/'):
            # redirect browser - doing basically what apache does
            self.send_response(HTTPStatus.MOVED_PERMANENTLY)
            new_parts = (parts[0], parts[1], parts[2] + '/',
                         parts[3], parts[4])
            new_url = urllib.parse.urlunsplit(new_parts)
            self.send_header("Location", new_url)
            self.end_headers()
            return None
        ...
    ...

The problem is that the "Location" header starts with "//".

As for the directory issue, not only .ssh but an attacker can use any directory to make the open redirection exploitable.

And as for the HTTP Header Location, the server does not remove extra trailing slash from the PAYLOAD uri, which seems to be the cause of vulnerability getting exploited.

http://127.0.0.1:8000//attacker.com/..%2f..%2f..%2f..%2f..%2f../%0a%0d/../.ssh

So I believe the server should check for multiple slashes and remove them from the path.

Additionally, as you've mentioned it should also prepend the host:port/ to the new_url variable before writing the HTTP Header Location because if an attacker bypasses the protection and add an extra slash the server will still redirect to the host which is getting inserted into the Location header. But honestly I need your opinion as concatenating host to the url may lead to Host Header Injection but it'll then require a different context.

Please watch the POC video. POC Video: https://youtu.be/rLfOoEu1XXg