Information Security 16 min read

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.

IT Services Circle
IT Services Circle
IT Services Circle
10 Unknown Security Pitfalls for Python

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 user

2. 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 False

7. 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.

pythonCode ReviewBest Practicessecurityvulnerabilities
IT Services Circle
Written by

IT Services Circle

Delivering cutting-edge internet insights and practical learning resources. We're a passionate and principled IT media platform.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.