GHSA-7432-952r-cw78: Authlib Vulnerable to JWE RSA1_5 Bleichenbacher Padding Oracle
1. Executive SummaryA cryptographic padding oracle vulnerability was identified in the Authlib Python library
concerning the implementation of the JSON Web Encryption (JWE) `RSA1_5` key management
algorithm. Authlib registers `RSA1_5` in its default algorithm registry without requiring
explicit opt-in, and actively destroys the constant-time Bleichenbacher mitigation that
the underlying `cryptography` library implements correctly.When `cryptography` encounters an invalid PKCS#1 v1.5 padding, it returns a randomized
byte string instead of raising an exception — the correct behavior per RFC 3218 §2.3.2.
Authlib ignores this contract and raises `ValueError('Invalid "cek" length')` immediately
after decryption, before reaching AES-GCM tag validation. This creates a clean, reliable
**Exception Oracle**:**Invalid padding** → `cryptography` returns random bytes → Authlib length check fails
→ `ValueError: Invalid "cek" length`**Valid padding, wrong MAC** → decryption succeeds → length check passes → AES-GCM
fails → `InvalidTag`**This oracle is active by default in every Authlib installation without any special
configuration by the developer or the attacker.** The three most widely used Python web
frameworks — Flask, Django, and FastAPI — all expose distinguishable HTTP responses for
these two exception classes in their default configurations, requiring no additional
setup to exploit.**Empirically confirmed on authlib 1.6.8 + cryptography 46.0.5:**2. Technical Details & Root Cause2.1 Vulnerable Code**File:** `authlib/jose/rfc7518/jwe_algs.py`2.2 Root Cause — Active Mitigation Destruction`cryptography` 46.0.5 implements the Bleichenbacher mitigation correctly at the library
level. When PKCS#1 v1.5 padding validation fails, it does not raise an exception.
Instead it returns a randomized byte string (empirically observed: 84 bytes for a
2048-bit RSA key). The caller is expected to pass this fake key to the symmetric
decryptor, where MAC/tag validation will fail in constant time — producing an error
indistinguishable from a MAC failure on a valid padding.Authlib does not honor this contract. The length check on the following line detects
that 84 bytes != 16 bytes (128-bit CEK for A128GCM) and raises `ValueError('Invalid
"cek" length')` immediately. This exception propagates before AES-GCM is ever reached,
creating two execution paths with observable differences:The single line `raise ValueError('Invalid "cek" length')` is the complete root cause.
Removing the raise and replacing it with a silent random CEK fallback eliminates both
the exception oracle and any residual timing difference.2.3 Empirical Confirmation**All results obtained on authlib 1.6.8 / cryptography 46.0.5 / Linux x86_64
running the attached PoC (`poc_bleichenbacher.py`):****Note on timing:** The 0.287ms delta is within the noise margin (stdev ~1ms across
50 iterations) and is not claimed as a reliable standalone timing oracle. The exception
oracle is the primary exploitable vector and does not require timing measurement.3. Default Framework Behavior — Why This Is Exploitable Out of the BoxA potential objection to this report is that middleware or custom error handlers could
normalize exceptions to a single HTTP response, eliminating the observable discrepancy.
This section addresses that objection directly.**The oracle is active in default configurations of all major Python web frameworks.**
No special server misconfiguration is required. The following demonstrates the default
behavior for Flask, Django, and FastAPI — the three most widely deployed Python web
frameworks — when an unhandled exception propagates from a route handler:Flask (default configuration)Flask's default error handler returns the exception message in the response body for
debug mode, and an empty 500 for production. However, even in production, the response
body content differs between `ValueError` (which has a message) and `InvalidTag`
(which has no message), leaking the oracle through response body length.FastAPI (default configuration)FastAPI normalizes both to HTTP 500 in production. However, FastAPI's default
`RequestValidationError` and `HTTPException` handlers do not catch arbitrary exceptions,
so the distinguishable stack trace is logged — and in many deployments, error monitoring
tools (Sentry, Datadog, etc.) expose the exception class to operators, enabling oracle
exploitation by an insider or via log exfiltration.Django REST Framework (default configuration)**Summary:** Even in cases where HTTP status codes are normalized, the oracle persists
through response body differences, response timing, or error monitoring infrastructure.
The RFC 3218 §2.3.2 requirement exists precisely because any observable difference —
regardless of channel — is sufficient for a Bleichenbacher attack. The library is
responsible for eliminating the discrepancy at the source, not delegating that
responsibility to application developers.**This is a library-level vulnerability.** Requiring every application developer to
implement custom exception normalization to compensate for a cryptographic flaw in
the library violates the principle of secure defaults. The fix must be in Authlib.4. Specification ViolationsRFC 3218 — Preventing the Million Message Attack on CMS**Section 2.3.2 (Mitigation):**"The receiver MUST NOT return any information that indicates whether the decryption
failed because the PKCS #1 padding was incorrect or because the MAC was incorrect."This is an absolute requirement with no exceptions for "application-level mitigations."
Authlib violates this by raising a different exception class for padding failures than
for MAC failures. The `cryptography` library already implements the correct mitigation
for this exact scenario — Authlib destroys it with a single length check.RFC 7516 — JSON Web Encryption**Section 9 (Security Considerations):**"An attacker who can cause a JWE decryption to fail in different ways based on the
structure of the encrypted key can mount a Bleichenbacher attack."Authlib enables exactly this scenario. Two structurally different encrypted keys
(one with invalid padding, one with valid padding but wrong CEK) produce two different
exception classes. This is the exact condition RFC 7516 §9 warns against.5. Attack ScenarioThe attacker identifies an Authlib-powered endpoint that decrypts JWE tokens.
Because `RSA1_5` is in the default registry, **no special server configuration
is required**.The attacker obtains the server RSA public key — typically available via the
JWKS endpoint (`/.well-known/jwks.json`), which is standard in OIDC deployments.The attacker crafts JWE tokens with the `RSA1_5` algorithm and submits a stream
of requests to the endpoint, manipulating the `ek` component per Bleichenbacher's
algorithm.The server responds with observable differences between the two paths:`ValueError` path → distinguishable response (exception message, timing, or
error monitoring artifact)`InvalidTag` path → different distinguishable responseBy observing these oracle responses across thousands of requests, the attacker
geometrically narrows the PKCS#1 v1.5 plaintext boundaries until the CEK is
fully recovered.With the CEK recovered:Any intercepted JWE payload can be decrypted without the RSA private key.New valid JWE tokens can be forged using the recovered CEK.**Prerequisites:**Target endpoint accepts JWE tokens with `RSA1_5` (active by default)Any observable difference exists between the two error paths at the HTTP layer
(present by default in Flask, Django, FastAPI without custom error handling)Attacker can send requests at sufficient volume (rate limiting may extend attack
duration but does not prevent it)6. Remediation6.1 Immediate — Remove RSA1_5 from Default RegistryRemove `RSA1_5` from the default `JWE_ALG_ALGORITHMS` registry. Users requiring
legacy RSA1_5 support should explicitly opt-in with a documented security warning.
This eliminates the attack surface for all users not requiring this algorithm.6.2 Code Fix — Restore Constant-Time BehaviorThe `unwrap` method must never raise an exception that distinguishes padding failure
from MAC failure. The length check must be replaced with a silent random CEK fallback,
preserving the mitigation that `cryptography` implements.**Suggested Patch (`authlib/jose/rfc7518/jwe_algs.py`):****Result:** Both paths return a CEK of the correct length. AES-GCM tag validation
fails for both, producing `InvalidTag` in both cases. The exception oracle is
eliminated. Empirically validated via TEST 5 of the attached PoC.7. Proof of Concept**Setup:**See attached `poc_bleichenbacher.py`. All 5 tests run against the real installed
authlib module without mocks.**Confirmed Output (authlib 1.6.8 / cryptography 46.0.5 / Linux x86_64):**CodeOutput
CVSS v4.0: 8.3