CVE-2023-24329 – URL Blackslisting Bypass in Python's urllib.parse
A vulnerability in Python's urllib.parse module that allows attackers to bypass URL blocklists simply by adding leading whitespace before a URL.
Introduction
Python is one of the most widely used programming languages, and its standard library powers countless web applications. Because developers often trust these built-in libraries, subtle parsing bugs can have security implications across many projects.
In this article, we’ll take an in-depth look at CVE-2023-24329, a vulnerability in Python’s urllib.parse module that allows attackers to bypass URL blocklists simply by adding leading whitespace before a URL.
We’ll reproduce the vulnerability, analyze the root cause by stepping through the CPython source code, and examine how the issue was fixed.
CVE Information
CVE: CVE-2023-24329
Affected Versions
- Python ≤ 3.11.3
- Fixed in Python 3.11.4
Description
An issue in Python’s urllib.parse module allows attackers to bypass URL blocklists by supplying URLs that begin with blank characters. Applications relying on urllib.parse.urlparse() to validate or restrict URL schemes or hostnames may incorrectly classify malicious URLs as safe.
Why This Matters
Many web applications accept user-supplied URLs for features such as:
- URL previews
- Webhooks
- File fetching
- Image downloading
- Proxy endpoints
- Server-side requests (SSRF protection)
A common defense is to parse the URL and reject dangerous schemes such as:
file://ftp://gopher://data://
or block specific hostnames.
Applications frequently implement checks similar to:
1
2
3
4
scheme = urllib.parse.urlparse(url).scheme
if scheme == "file":
reject()
The assumption is that urlparse() will always correctly identify the scheme.
CVE-2023-24329 breaks that assumption.
Reproducing the Vulnerability
For this demonstration, I used Python 3.11.3, the final release before the vulnerability was patched.
We’ll build a small Flask application that accepts a URL from the user and blocks dangerous schemes before opening it.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
from flask import Flask, render_template, request
import urllib.request
import urllib.error
import urllib.parse
app = Flask(__name__)
def safeURLOpener(inputLink):
block_schemes = [
"file",
"gopher",
"expect",
"php",
"dict",
"ftp",
"glob",
"data"
]
block_host = [
"instagram.com",
"youtube.com",
"tiktok.com"
]
input_scheme = urllib.parse.urlparse(inputLink).scheme
input_hostname = urllib.parse.urlparse(inputLink).hostname
if input_scheme in block_schemes:
return "Input scheme is forbidden"
if input_hostname in block_host:
return "Input hostname is forbidden"
try:
target = urllib.request.urlopen(inputLink)
return target.read().decode("utf-8")
except urllib.error.URLError as e:
return f"Error opening URL: {e}"
@app.route("/", methods=["GET", "POST"])
def index():
content = ""
if request.method == "POST":
url = request.form.get("domain")
if url:
content = safeURLOpener(url)
return render_template("index.html", content=content)
if __name__ == "__main__":
app.run(debug=True)
Running the application starts a local server at:
1
http://127.0.0.1:5000
Expected Behavior
Suppose we submit the following payload:
1
file:///C:/Windows/win.ini
The application correctly detects the file scheme and rejects it.
1
Input scheme is forbidden
So far, everything works as intended.
Bypassing the Blocklist
Now prepend a single space:
1
file:///C:/Windows/win.ini
Despite being almost identical, the application no longer recognizes the URL as using the file scheme.
Instead, the parser treats the entire string as a normal path, allowing the validation logic to succeed and the dangerous URL to be processed.
A single leading blank character completely bypasses the blocklist.
Investigating the Vulnerability
To understand why this happens, we’ll inspect the CPython implementation.
The vulnerable source code can be found in the CPython repository:
https://github.com/python/cpython/tree/3.11/Lib/urllib
We’ll use a simple example:
1
2
3
4
5
from urllib.parse import urlparse
url = " https://www.example.com"
print(urlparse(url))
Set a breakpoint on the urlparse() call and step into the implementation.
Eventually, the debugger enters:
1
urllib/parse.py
Inside urlparse()
The urlparse() function first prepares the arguments and then delegates most of the parsing work to urlsplit().
The call sequence is roughly:
1
2
3
4
5
urlparse()
│
├── _coerce_args()
│
└── urlsplit()
_coerce_args()
This helper simply ensures the supplied arguments are all strings (or all bytes).
If the types don’t match, it raises an exception.
Nothing security-related happens here.
urlsplit()
This is where the interesting logic lives.
Before parsing begins, Python removes a few unsafe characters using:
1
UNSAFE_URL_BYTES_TO_REMOVE
These characters include:
\t\r\n
This prevents several injection-related attacks.
Notice what’s missing:
Spaces are not removed.
Detecting the Scheme
The parser searches for the first colon:
1
url.find(":")
Everything before the colon is treated as a potential scheme.
Python then validates every character:
1
for c in url[:i]:
Each character must belong to the allowed character set stored in:
1
scheme_chars
When the URL begins with a blank character, the first character is not a valid scheme character.
Instead of rejecting the URL, the parser simply fails to identify a scheme and continues parsing as though no scheme exists.
As a result:
1
scheme = ""
and the remainder of the URL is interpreted as part of the path.
This is the core of CVE-2023-24329.
Continuing the Parsing Process
Since no scheme is detected, parsing proceeds normally.
The parser then:
- checks for a network location (
//) - validates IPv6 syntax
- extracts fragments (
#) - extracts query parameters (
?) - validates the hostname through
_checknetloc()
Finally, it constructs a ParseResult.
Because no scheme was ever recognized, the resulting object looks roughly like:
1
2
3
4
5
6
7
8
ParseResult(
scheme='',
netloc='',
path=' file:///C:/Windows/win.ini',
params='',
query='',
fragment=''
)
The dangerous URL is now hiding inside the path field rather than the scheme field.
Applications checking only:
1
if parsed.scheme == "file":
will incorrectly conclude the URL is safe.
Root Cause
The vulnerability stems from how urlsplit() handles invalid characters before the scheme.
Rather than rejecting the input, it silently abandons scheme parsing and continues processing the URL.
This behavior causes security checks that rely on parsed.scheme or parsed.hostname to fail.
The Patch
The fix was introduced in Python 3.11.4.
Patch:
https://github.com/python/cpython/pull/99421
The update expands validation and introduces additional test cases covering malformed URL schemes.
The new tests verify that invalid prefixes—including leading whitespace, invalid symbols, digits, and non-ASCII characters—are handled correctly rather than silently ignored.
This prevents attackers from bypassing validation using malformed URLs.
Mitigation
If your application depends on urllib.parse for security decisions:
- Upgrade to Python 3.11.4 or later.
- Avoid relying solely on blocklists.
- Normalize and trim user input before parsing.
- Prefer allowlists of approved schemes (such as
httpandhttps) instead of attempting to block dangerous ones.
Final Thoughts
CVE-2023-24329 is an excellent example of how seemingly harmless parsing behavior can introduce meaningful security risks.
The vulnerability isn’t caused by complex memory corruption or advanced exploitation techniques. Instead, it comes down to a simple assumption: developers trusted that urlparse() would always identify a URL scheme correctly.
A single leading space was enough to break that assumption.
This serves as an important reminder that parsing libraries are not security boundaries by themselves. When building security-sensitive applications, validation should be explicit, input should be normalized before inspection, and dependencies should be kept up to date.
Sometimes, the smallest parsing edge case can become a surprisingly effective security bypass.
References
Python Issue Tracker — CVE-2023-24329 https://github.com/python/cpython/issues/102153
Patch introducing the fix https://github.com/python/cpython/pull/99421
CPython
urllib.parsesource https://github.com/python/cpython/tree/3.11/Lib/urllib