How to Prevent Timing Attacks in Python
Timing attacks are side-channel exploits where an adversary infers
secret information—such as API keys, authentication tokens, or password
hashes—by measuring the slight variations in time a server takes to
validate user input. In standard programming operations, the equality
operator (==) evaluates strings lazily, exiting immediately
upon finding the first non-matching character. This article outlines the
mechanisms Python provides to eliminate these execution time differences
through constant-time string comparisons, primarily utilizing built-in
functions designed for cryptographic safety.
The Flaw of Standard Comparison
The standard equality operator in Python (==) uses an
early-exit strategy for performance optimization. When comparing two
strings, Python checks characters sequentially from left to right:
# Vulnerable comparison
if user_token == actual_token:
grant_access()If the first character does not match, the check finishes in fewer CPU cycles than if the mismatch occurs at the tenth character. An attacker with precise network measurements or local access can send systematic guesses and deduce the secret token character-by-character based on latency differences.
The Solution:
secrets.compare_digest
Python 3.6 introduced the secrets module to handle
security-sensitive data generation and validation. The standard
mechanism for preventing timing attacks is
secrets.compare_digest(a, b).
import secrets
# Constant-time comparison
if secrets.compare_digest(user_token, actual_token):
grant_access()This function ensures that the comparison executes in constant time
relative to the length of the strings, eliminating early-exit timing
leaks. It accepts both ASCII-encoded str objects and
bytes-like objects, but both inputs must be of the same
type to be evaluated.
The Alternative:
hmac.compare_digest
For environments running earlier versions of Python (Python 2.7.7+
and Python 3.3+), the same functionality is available through the
hmac module:
import hmac
if hmac.compare_digest(user_token, actual_token):
grant_access()In modern Python releases, secrets.compare_digest is
simply an alias for hmac.compare_digest. Both invoke the
same underlying C implementation.
How Constant-Time Comparison Works
At the C runtime level, compare_digest avoids
conditional jumps based on character values. Instead of halting at the
first mismatch, the algorithm iterates across the entirety of the buffer
and combines differences using bitwise operations:
- It checks whether the inputs have matching lengths.
- It iterates through every byte pair, computing the bitwise XOR
(
^) between them. - It accumulates the XOR results using a bitwise OR (
|) into a single variable. - If the final accumulated value is zero, the strings are identical; if it is non-zero, they differ.
Because the CPU executes the loop for the full sequence regardless of where differences occur, the execution time remains constant.
Mitigating Length Leakage
While compare_digest prevents content-based timing
attacks, it can still leak the length of the secret string, as
the loop duration depends on the length of the inputs. If token lengths
must remain confidential, both the user input and the target secret
should be hashed using a fixed-length cryptographic hash function (such
as SHA-256) prior to comparison:
import hashlib
import secrets
def secure_token_check(provided_token: str, real_token: str) -> bool:
# Hash both inputs to guarantee identical lengths
hash_provided = hashlib.sha256(provided_token.encode()).digest()
hash_real = hashlib.sha256(real_token.encode()).digest()
# Compare fixed-length digests in constant time
return secrets.compare_digest(hash_provided, hash_real)By standardizing the input length before passing the data to
compare_digest, this approach closes both content and
length-based timing vulnerabilities.