10 Unknown Security Pitfalls for Python
This article outlines ten lesser‑known Python security pitfalls—from optimized‑away asserts and directory permission quirks to path traversal, regex misuse, Unicode normalization attacks, and IP address normalization—illustrating how subtle language features can lead to serious vulnerabilities in real‑world applications.
1. Optimized‑away assert statements
When Python code is run with optimizations (the -O flag), all assert statements are stripped out. Developers sometimes rely on assert for security checks, which can be bypassed in optimized builds, allowing non‑privileged users to execute privileged actions.
def superuser_action(request, user):
assert user.is_super_user
# execute action as super user2. os.makedirs permission quirks
The os.makedirs function accepts a mode argument. In Python < 3.6, all created directories inherit the specified mode (e.g., 0o700 ). In Python ≥ 3.6 only the final directory receives the mode, while intermediate directories get the default 0o755 , which can lead to permission‑escalation bugs such as CVE‑2022‑24583.
def init_directories(request):
os.makedirs("A/B/C", mode=0o700)
return HttpResponse("Done!")3. Absolute‑path joining with os.path.join
If any component passed to os.path.join starts with a slash, all previous components are discarded and the result is treated as an absolute path. This can turn a seemingly safe path construction into a path‑traversal vulnerability.
def read_file(request):
filename = request.POST['filename']
file_path = os.path.join("var", "lib", filename)
if file_path.find(".") != -1:
return HttpResponse("Failed!")
with open(file_path) as f:
return HttpResponse(f.read(), content_type='text/plain')4. Arbitrary temporary files
tempfile.NamedTemporaryFile accepts prefix and suffix arguments that can be manipulated for directory‑traversal attacks, allowing an attacker to create temporary files in arbitrary locations.
def touch_tmp_file(request):
id = request.GET['id']
tmp_file = tempfile.NamedTemporaryFile(prefix=id)
return HttpResponse(f"tmp file: {tmp_file} created!", content_type='text/plain')5. Extended Zip Slip
Using zipfile.ZipFile without proper sanitisation of entry names can lead to arbitrary file writes. Functions like zipfile.extract and extractall perform sanitisation, but manual extraction loops do not.
def extract_html(request):
filename = request.FILES['filename']
zf = zipfile.ZipFile(filename.temporary_file_path(), "r")
for entry in zf.namelist():
if entry.endswith('.html'):
file_content = zf.read(entry)
with open(entry, "wb") as fp:
fp.write(file_content)
zf.close()
return HttpResponse("HTML files extracted!")6. Incomplete regular‑expression matching
Using re.match instead of re.search for blacklist patterns can miss malicious input that contains newlines, allowing attackers to bypass simple SQL‑injection checks.
def is_sql_injection(request):
pattern = re.compile(r".*(union)|(select).*")
name_to_test = request.GET['name']
if re.search(pattern, name_to_test):
return True
return False7. Unicode normalisation bypass
Normalising user input with unicodedata.normalize('NFKC', ...) after escaping can turn encoded characters into dangerous symbols (e.g., turning "%EF%B9%A4" into "<"), re‑enabling XSS attacks.
def render_input(request):
user_input = escape(request.GET['p'])
normalized_user_input = unicodedata.normalize("NFKC", user_input)
context = {'my_input': normalized_user_input}
return render(request, 'test.html', context)8. Unicode code‑point collisions
Different Unicode characters can map to the same code point after case folding, causing email‑address look‑ups to succeed for visually similar but different strings, as demonstrated in a Django password‑reset bug (CVE‑2019‑19844).
def reset_pw(request):
email = request.GET['email']
result = User.objects.filter(email__exact=email.upper()).first()
if not result:
return HttpResponse("User not found!")
send_mail('Reset Password', 'Your new pw: 123456.', '[email protected]', [email], fail_silently=False)
return HttpResponse("Password reset email send!")9. IP address normalisation
Python < 3.8's ipaddress.IPv4Address normalises addresses, stripping leading zeros. An attacker can supply "127.0.001" to bypass a blacklist that only contains "127.0.0.1", leading to SSRF.
def send_request(request):
ip = request.GET['ip']
try:
if ip in ["127.0.0.1", "0.0.0.0"]:
return HttpResponse("Not allowed!")
ip = str(ipaddress.IPv4Address(ip))
except ipaddress.AddressValueError:
return HttpResponse("Error at validation!")
requests.get('https://' + ip)
return HttpResponse("Request send!")10. URL query‑parameter parsing
In Python < 3.7, urllib.parse.parse_qsl treats both ';' and '&' as separators. When a front‑end (e.g., PHP) forwards a query like ?a=1;b=2 unchanged, the Python back‑end will interpret it as two parameters, potentially causing cache‑poisoning or other logic errors.
Overall, these ten subtle Python behaviours can easily be overlooked but have caused real‑world security incidents; developers should stay aware, read documentation carefully, and keep dependencies up to date.
IT Services Circle
Delivering cutting-edge internet insights and practical learning resources. We're a passionate and principled IT media platform.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.