aio-libs/aiohttp · error · ClientError
Security issue: Digest auth challenge contains empty…
Error message
Security issue: Digest auth challenge contains empty 'nonce' value
What it means
Raised when a Digest challenge contains a 'nonce' key but its value is the empty string. Unlike realm (which RFC 7616 permits empty), an empty nonce breaks replay protection entirely, so aiohttp treats it as a security defect and refuses to proceed. The guard sits in _encode() immediately after nonce is read from the challenge dict.
Solutions
- Capture the exact WWW-Authenticate header to confirm nonce is present-but-empty.
- Fix the server to generate and emit a real opaque nonce value per RFC 7616.
- Report the issue to the server vendor if it is not under your control; do not attempt to weaken the client to accept it.
- Fall back to a different auth scheme supported by the server.
Example fix
# before — server sends: WWW-Authenticate: Digest realm="x", nonce=""
await session.get('https://srv/secret')
# after — server must send a populated nonce
# WWW-Authenticate: Digest realm="x", nonce="OA9BSiR4b..." Defensive patterns
Strategy: try-catch
Validate before calling
import re
def nonce_is_safe(www_authenticate: str) -> bool:
m = re.search(r'nonce="?([^",\s]+)"?', www_authenticate)
return bool(m and m.group(1)) Type guard
def nonce_nonempty(challenge_header: str) -> bool:
import re
m = re.search(r'nonce="?([^"\s,]*)"?', challenge_header, re.I)
return m is not None and len(m.group(1)) > 0 Try / catch
from aiohttp import ClientError
try:
resp = await session.get(url)
except ClientError as e:
if 'empty' in str(e) and 'nonce' in str(e):
# security-sensitive: do not weaken; report to server owner
raise SecurityWarning('Server issued empty Digest nonce')
raise Prevention
- Never attempt to bypass this guard — an empty nonce defeats replay protection.
- Report empty-nonce servers to their operators as a security defect.
- Include a nonce-presence/non-empty assertion in server integration tests.
When it happens
Trigger: Server returns 'WWW-Authenticate: Digest realm="x", nonce=""' (key present, value empty). The middleware reaches the 'if not nonce' check in _encode() and raises ClientError before any hash is computed.
Common situations: Server bug emitting an empty nonce; template/placeholder bug in a generated challenge; security testing where a fuzzer mutates the challenge; proxy that blank-strips unknown fields.
Related errors
- Digest auth error: Unsupported hash algorithm
- Digest auth error: Unsupported Quality of Protection (qop)…
- Malformed Digest auth challenge: Missing 'nonce' parameter
- Malformed Digest auth challenge: Missing 'realm' parameter
- A ":" is not allowed in username (RFC 1945#section-11.1)
AI-assisted analysis of aio-libs/aiohttp@d041d4d0fd (2026-08-11).
Data as JSON: /api/errors/fa3ff112b1e272e7.
Report an issue: GitHub.
Appendix: source
Thrown at aiohttp/client_middleware_digest_auth.py:258
"""
challenge = self._challenge
if "realm" not in challenge:
raise ClientError(
"Malformed Digest auth challenge: Missing 'realm' parameter"
)
if "nonce" not in challenge:
raise ClientError(
"Malformed Digest auth challenge: Missing 'nonce' parameter"
)
# Empty realm values are allowed per RFC 7616 (SHOULD, not MUST, contain host name)
realm = challenge["realm"]
nonce = challenge["nonce"]
# Empty nonce values are not allowed as they are security-critical for replay protection
if not nonce:
raise ClientError(
"Security issue: Digest auth challenge contains empty 'nonce' value"
)
qop_raw = challenge.get("qop", "")
# Preserve original algorithm case for response while using uppercase for processing
algorithm_original = challenge.get("algorithm", "MD5")
algorithm = algorithm_original.upper()
opaque = challenge.get("opaque", "")
# Convert string values to bytes once
nonce_bytes = nonce.encode("utf-8")
realm_bytes = realm.encode("utf-8")
# Use the encoded request-target (raw_path_qs) since that is what is
# transmitted on the wire and what the server signs against. Using the
# decoded form would cause digest verification to fail when the path
# or query string contains percent-encoded reserved characters.
path = URL(url).raw_path_qs
View on GitHub (pinned to d041d4d0fd)