To the Python Cryptographic Authority / cryptography maintainers,

I am a security researcher at CodeAnt AI. I have discovered a
denial-of-service vulnerability in the cryptography package's PBKDF2HMAC
key derivation function. When iterations=0 is passed, the Rust backend
raises a pyo3_runtime.PanicException, a direct subclass of BaseException,
not Exception — which silently escapes all standard Python exception
handlers and can crash the application process.

Package: cryptography
Affected Versions: All modern versions (36.x through 46.x) using the
PyO3/Rust backend
Tested Version: 46.0.5

CVSS: 5.3 MEDIUM (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L)
CWE: CWE-703 (Improper Check or Handling of Exceptional Conditions), CWE-20
(Improper Input Validation)

---

What is the Issue?

PBKDF2HMAC.__init__() in cryptography/hazmat/primitives/kdf/pbkdf2.py
stores the iterations parameter directly without any bounds validation:

    def __init__(self, algorithm, length, salt, iterations, backend=None):
        ...
        self._iterations = iterations   # stored directly, no bounds check

When derive() is called with iterations=0, this value is passed to the Rust
layer in src/rust/src/backend/kdf.rs:30, which calls .unwrap() on the
OpenSSL error rather than propagating it via map_err. This causes a Rust
panic:

    thread '<unnamed>' panicked at src/rust/src/backend/kdf.rs:30:87:
    called `Result::unwrap()` on an `Err` value: ErrorStack([Error { code:
478150779,
      library: "Provider routines",
      function: "kdf_pbkdf2_set_ctx_params",
      reason: "invalid iteration count",
      file: "providers/implementations/kdfs/pbkdf2.c", line: 305 }])

PyO3 surfaces Rust panics as pyo3_runtime.PanicException. This class
inherits from BaseException, NOT Exception:

    pyo3_runtime.PanicException → BaseException → object

---

What is the Impact?

1. Process crash via DoS: Any application that accepts a user-controlled
iterations value and passes it to PBKDF2HMAC (e.g., via JSON API payload,
config file, URL parameter, or database-stored value) can be crashed by a
single request with iterations=0.

2. Silent bypass of all error handling: The universally expected Python
idiom "except Exception:" does NOT catch BaseException subclasses. Every
standard web framework error handler, request handler, and try/except block
in the application stack silently fails to catch this crash:

    try:
        kdf = PBKDF2HMAC(algorithm=hashes.SHA256(), length=32,
salt=b"salt", iterations=0)
        result = kdf.derive(b"password")
    except Exception as e:
        print("Caught:", e)   # <-- NEVER REACHED

3. Contrast with iterations=-1: Passing -1 correctly raises OverflowError
(a subclass of Exception) and IS catchable. Only iterations=0 hits the Rust
panic path.

---

Proof of Concept:

    from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
    from cryptography.hazmat.primitives import hashes

    kdf = PBKDF2HMAC(algorithm=hashes.SHA256(), length=32, salt=b"salt",
iterations=0)
    print("__init__ passed: no error raised here")

    try:
        result = kdf.derive(b"password")
    except Exception as e:
        print("Caught (Exception):", e)   # NEVER REACHED

Realistic attack vector (API endpoint accepting PBKDF2 parameters):

    @app.post("/derive-key")
    async def derive_key(params: KDFParams):
        try:
            kdf = PBKDF2HMAC(
                algorithm=hashes.SHA256(),
                length=params.key_length,
                salt=params.salt.encode(),
                iterations=params.iterations   # user-controlled!
            )
            return {"key": kdf.derive(params.password.encode()).hex()}
        except Exception as e:
            raise HTTPException(status_code=400, detail=str(e))
        # PanicException bypasses the except Exception block above

---

Recommended Fix:

Option 1 — Python-level validation (fastest fix):

    def __init__(self, algorithm, length, salt, iterations, backend=None):
        ...
        if not isinstance(iterations, int) or iterations < 1:
            raise ValueError(
                "iterations must be a positive integer, got
{!r}".format(iterations)
            )
        self._iterations = iterations

Option 2 — Rust-level fix (proper long-term fix):
Replace .unwrap() in src/rust/src/backend/kdf.rs:30 with proper error
propagation using map_err so that OpenSSL errors surface as catchable
Python exceptions via PyO3's error conversion.

Both fixes should be applied: the Python-level check as a fast guard with a
clear error message, and the Rust-level fix to make the entire error path
robust.

---

I am happy to provide a complete test harness, coordinate a patch review,
or adjust the disclosure timeline.

---

Researcher: CodeAnt AI Security Research Team, part of
https://www.codeant.ai/
Contact: [email protected]
_______________________________________________
Cryptography-dev mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/cryptography-dev.python.org
Member address: [email protected]

Reply via email to