August 2, 2022·4 min read·engineeringpythonhttpsecurity

Python 3.10.6 normalizes paths before HTTP redirects

Python 3.10.6 closes an open redirect in http.server. The repair handles a mismatch between local path processing and a browser's interpretation of a Location header.

A path that looks local to a file server can name a different host when a browser interprets it as a redirect destination. Python 3.10.6 repairs that mismatch in http.server. A request target beginning with two slashes could lead to an open redirect because clients interpret that form as a URL with an authority and no explicit scheme.

The August 2 release includes the repair as a security change. Its version-specific changelog credits Hamza Avvan with discovering the vulnerability and proposing the initial fix. The implementation addresses the ambiguity before the handler can reuse the path in a response.

I appreciate Python's ability to make a small utility available through its standard library. That convenience does not remove the protocol details the utility must implement. This fix is useful because it resolves an interpretation difference at a shared parsing boundary, where individual applications should not have to rediscover it.

One string has two interpretations

The server implementation at the release tag explains the problem in its request parser. HTTP clients can treat //path as an absolute URI without a scheme. The server must therefore avoid treating a request target with that prefix as an ordinary path and later emitting it unchanged into URL-processing code.

The repair reduces an initial run of slashes to one slash. It happens after the parser records the command and path from the request line. Normalizing at that point gives subsequent request handling an unambiguous leading path separator.

path = "//example.invalid/directory"
if path.startswith("//"):
    path = "/" + path.lstrip("/")

assert path == "/example.invalid/directory"

This excerpt demonstrates the transformation used by the repair without starting a server or contacting a host. The host-like text remains part of the path after normalization. It no longer occupies the authority position that a URL consumer can recognize after two leading slashes.

The transformation is deliberately conditional. It does not remove every slash from a path, and it does not rewrite ordinary internal path separators. The specific ambiguity lies at the beginning of the request target. Replacing that prefix with a single slash changes the interpretation that matters to the reported vulnerability.

Directory redirects reuse the request target

The file-serving handler's send_head method shows why the parser's decision matters. It first translates the request path to a filesystem path. If that path identifies a directory and the URL lacks a trailing slash, the handler constructs a redirect that adds one.

That behavior is ordinary directory serving. The implementation parses the request target into URL components, appends a slash to the path component and reconstructs the result for the Location header. It preserves other URL components in the process. Correctness depends on those components having the intended meaning before reconstruction.

An unchecked double-slash prefix disrupts that assumption. URL parsing can interpret text as an authority even when the file-serving path was meant to stay local. Appending a directory slash does not remove that authority. The reconstructed redirect can therefore express a destination outside the server's intended local path.

The new normalization occurs before this downstream logic. Directory handling can continue expressing its trailing-slash rule, while the parser prevents the particular prefix from acquiring a different meaning during URL reconstruction. The repair targets the boundary where the conflicting interpretations begin.

Shared parsing affects subclasses

The module documentation at the same tag describes BaseHTTPRequestHandler as the component that parses the request and headers before invoking a method-specific handler. Its path attribute includes the query component when one exists. Subclasses consume state prepared by that common parser.

That arrangement is why a parser-level fix matters beyond one local redirect expression. Request-method implementations receive the normalized path through the normal handler interface. The common component owns the initial interpretation rather than requiring every subclass to apply an equivalent prefix rule.

This is also a useful limit on the claim. The fix addresses an open redirect caused by this path prefix. It does not make all URL construction safe in arbitrary subclasses, and it does not establish a policy for application redirects supplied through other inputs. Those operations have their own contracts and sources of data.

I regard this as a maintenance repair with a clear mechanism. The change is small because the ambiguous prefix is small, but the reasoning spans HTTP request parsing, filesystem translation and response URL construction. Reading only the final redirect line would miss the earlier assumption that made it unsafe.

The module's documentation continues to advise against production use because it implements only basic security checks. Within that stated scope, Python 3.10.6 corrects a concrete protocol error. Directory redirects still add their trailing slash, with the request path normalized before URL parsing can treat its prefix as an external authority.