skip to content

Security and Safe APIs

Unsafe defaults inside ordinary standard-library calls, grouped by where the risk enters: untrusted data, dynamic execution, the host, the crypto primitive, and what leaks back out.

part ofPythonoverview, primer and where to startread it →
on this pageshow

explore

questions

97 · 5 sections

Why is calling pickle.loads() on untrusted bytes the same as running that data?

level: juniorimportance: must knowfreq 70%
basics
~20 s

Unpickling is not parsing. A pickle stream is a tiny program whose opcodes import names and call them, so pickle.loads on attacker-controlled bytes can invoke any importable callable with attacker-chosen arguments before it returns anything.

open as a page

Why pass values to sqlite3.Cursor.execute as a parameter tuple instead of formatting them into the SQL text?

level: juniorimportance: must knowfreq 78%
basics
~20 s

Placeholders keep the statement and the data on separate channels. sqlite3 compiles the SQL text first, then binds each parameter into a slot of the compiled statement, so a value can never become syntax. String formatting merges the two.

open as a page

What does tarfile's 'data' extraction filter block, and why is it the 3.14 default?

level: middleimportance: must knowfreq 35%
basics
~20 s

The data filter refuses tar members that would land outside the destination directory, refuses links whose target is absolute or escapes, and rejects device and FIFO members, while clearing ownership and risky permission bits. Python 3.14 makes it the default for extraction.

open as a page

Why is str.format with an attacker-supplied template string a data-leak risk?

level: middleimportance: must knowfreq 42%
basics
~20 s

Python's format mini-language allows attribute access and indexing inside a placeholder, so whoever writes the template chooses what is read out of the objects you pass. A chain through a method's globals reaches module-level secrets.

open as a page

Why does Python's re pattern ^(\d+)+$ hang on a crafted 30-character string?

level: middleimportance: must knowfreq 55%
basics
~20 s

Python's re module uses a backtracking engine. Nesting one quantifier inside another over the same characters gives it exponentially many ways to split the input, so a 30-character string that ultimately fails to match costs about a billion steps.

open as a page

Why is Python's eval() a code-execution risk on a string that came from a user?

level: juniorimportance: must knowfreq 65%
basics
~20 s

eval() compiles and runs its text as a Python expression, so one expression can import a module and touch files, processes or sockets with the privileges of your process. Parse untrusted text; never evaluate it.

open as a page

Why is there no reliable way to sandbox untrusted Python inside the same interpreter process?

level: juniorimportance: must knowfreq 40%
basics
~20 s

The Python object graph is fully connected: from any value you can follow attributes back to the type system, the builtins, and every loaded module. No matter which names you hide, code can walk to them. Real isolation is an OS-level boundary — a separate process, container, or VM.

open as a page

Why does importlib.import_module on a user-supplied name amount to arbitrary code execution?

level: middleimportance: must knowfreq 55%
basics
~20 s

Importing a module executes that module's top-level code. If the caller names the module, the caller chooses which code runs, and search order plus every installed distribution decide what that name resolves to. Import from a fixed allowlist instead.

open as a page

Which expressions does ast.literal_eval accept, and which does it refuse?

level: middleimportance: must knowfreq 60%
basics
~10 s

ast.literal_eval evaluates only literal structures: strings, bytes, numbers, booleans, None, Ellipsis, and the tuple, list, dict and set displays built from them. Any name, attribute, operator or function call raises ValueError instead of running.

open as a page

How can Python code with no builtins reach os.system by walking __class__ and __subclasses__?

level: middleimportance: must knowfreq 45%
basics
~20 s

Every object exposes class, and from there bases reaches object, whose subclasses() lists every class the interpreter has already loaded. An attacker scans that list for a gadget class whose method or globals reaches os or subprocess, so stripping builtins never removes the path back to dangerous code.

open as a page

Where do the entries in Python's os.environ come from, and who controls them?

level: juniorimportance: must knowfreq 55%
basics
~20 s

The process that launched yours supplies the environment block, and Python copies it into os.environ when the os module is first imported. Whoever controls the launch - a shell, a container spec, a job scheduler - controls the values.

open as a page

In subprocess.run, what changes between passing a list and passing shell=True?

level: juniorimportance: must knowfreq 75%
basics
~20 s

With a list, CPython execs the program directly and each element becomes exactly one argv entry, so nothing tokenises it. With shell=True the whole string goes to /bin/sh -c, where ; | $() and word splitting are live syntax.

open as a page

Why is it risky to run a Python script from a world-writable directory?

level: juniorimportance: must knowfreq 60%
basics
~20 s

Python puts the script's own directory first on sys.path, ahead of the standard library. Anyone who can write there can drop a file named like a module the script imports, and their code runs at import time.

open as a page

Why does os.path.join('/srv/uploads', name) not guarantee a path inside /srv/uploads?

level: juniorimportance: must knowfreq 55%
basics
~20 s

os.path.join throws away everything to the left of a component that is already absolute, so joining '/etc/passwd' onto a base returns '/etc/passwd'. A relative name holding '..' also climbs out. Joining concatenates; it never confines.

open as a page

Why is tempfile.mktemp unsafe where tempfile.mkstemp is not?

level: middleimportance: must knowfreq 52%
basics
~20 s

tempfile.mktemp only returns a name. Between that return and your own open() an attacker can create that path, usually as a symlink. tempfile.mkstemp creates and opens the file itself in one exclusive step, so there is no window to exploit.

open as a page

How does hashlib's update, digest and hexdigest cycle hash data incrementally?

level: juniorimportance: must knowfreq 65%
basics
~20 s

A hashlib hash object accumulates bytes across repeated update() calls, so feeding data in chunks matches hashing it in one call. digest() returns the raw bytes, hexdigest() the same value as a hex string; reading either does not reset the object.

open as a page

Which hashlib functions are built for password storage, and why not hashlib.sha256?

level: juniorimportance: must knowfreq 68%
basics
~20 s

hashlib offers two password functions: pbkdf2_hmac and scrypt. Both take a per-user salt and a work factor you choose, so one guess costs real time. hashlib.sha256 is built to be fast, which is exactly wrong for passwords.

open as a page

Why must a password-reset token come from `secrets`, not `random`?

level: juniorimportance: must knowfreq 70%
basics
~20 s

The random module's default generator is a Mersenne Twister whose entire internal state can be reconstructed from a few hundred observed outputs, so later tokens become predictable. secrets draws each token from the operating system's cryptographic source instead.

open as a page

Why does `hmac.compare_digest` exist when `==` already compares two bytes objects?

level: juniorimportance: must knowfreq 50%
basics
~20 s

== on two bytes objects stops at the first differing byte, so the time it takes reveals how much of a secret an attacker guessed right. hmac.compare_digest always does the same work; secrets.compare_digest is the same function.

open as a page

What does setting ssl.SSLContext.verify_mode to ssl.CERT_NONE actually give up?

level: juniorimportance: must knowfreq 60%
basics
~10 s

It turns off certificate verification, so a Python client accepts any certificate at all, including one an attacker generated. The connection stays encrypted but is no longer authenticated, which defeats the point of TLS.

open as a page

Why must traceback.format_exc() output never be sent in an HTTP response body?

level: juniorimportance: must knowfreq 60%
basics
~20 s

A formatted traceback exposes absolute file paths, the module layout, the failing source lines and sometimes the repr of local values such as connection strings. Log it server side under an opaque reference id and return only that id with a generic message.

open as a page

Why pass %s arguments to logging.Logger.info instead of building an f-string?

level: middleimportance: must knowfreq 55%
basics
~20 s

The logging module interpolates %s arguments only when a record is actually emitted, so a filtered-out call costs nothing and the raw values stay on LogRecord.args, where a redaction filter can still rewrite them. An f-string bakes them in first.

open as a page

How do you keep an API key field out of a Python dataclass's repr()?

level: middleimportance: must knowfreq 50%
basics
~20 s

Declare the attribute with dataclasses.field(repr=False). The generated repr then omits it, and str() omits it too because it falls back to repr. The value is still an ordinary attribute, so dataclasses.asdict(), vars() and pickling still carry it.

open as a page

What happens to assert statements and __debug__ when Python runs with -O?

level: middleimportance: must knowfreq 50%
basics
~20 s

The -O flag sets the builtin __debug__ to False and makes the compiler omit every assert statement from the bytecode entirely. A check written as an assert then guards nothing, so an assert must never carry input validation or a security decision.

open as a page

Why read a password with getpass.getpass() instead of input() in a Python CLI?

level: juniorimportance: should knowfreq 35%
basics
~20 s

getpass.getpass() turns terminal echo off while the user types, so the password never appears on screen or in a terminal recording, and it reads from the controlling terminal rather than a redirected stdin. input() echoes every character.

open as a page