{"record":{"id":"fa3ff112b1e272e7","repo":"aio-libs/aiohttp","slug":"security-issue-digest-auth-challenge-contains-emp","errorCode":null,"errorMessage":"Security issue: Digest auth challenge contains empty 'nonce' value","messagePattern":"Security issue: Digest auth challenge contains empty 'nonce' value","errorType":"exception","errorClass":"ClientError","httpStatus":null,"severity":"error","filePath":"aiohttp/client_middleware_digest_auth.py","lineNumber":258,"sourceCode":"        \"\"\"\n        challenge = self._challenge\n        if \"realm\" not in challenge:\n            raise ClientError(\n                \"Malformed Digest auth challenge: Missing 'realm' parameter\"\n            )\n\n        if \"nonce\" not in challenge:\n            raise ClientError(\n                \"Malformed Digest auth challenge: Missing 'nonce' parameter\"\n            )\n\n        # Empty realm values are allowed per RFC 7616 (SHOULD, not MUST, contain host name)\n        realm = challenge[\"realm\"]\n        nonce = challenge[\"nonce\"]\n\n        # Empty nonce values are not allowed as they are security-critical for replay protection\n        if not nonce:\n            raise ClientError(\n                \"Security issue: Digest auth challenge contains empty 'nonce' value\"\n            )\n\n        qop_raw = challenge.get(\"qop\", \"\")\n        # Preserve original algorithm case for response while using uppercase for processing\n        algorithm_original = challenge.get(\"algorithm\", \"MD5\")\n        algorithm = algorithm_original.upper()\n        opaque = challenge.get(\"opaque\", \"\")\n\n        # Convert string values to bytes once\n        nonce_bytes = nonce.encode(\"utf-8\")\n        realm_bytes = realm.encode(\"utf-8\")\n        # Use the encoded request-target (raw_path_qs) since that is what is\n        # transmitted on the wire and what the server signs against. Using the\n        # decoded form would cause digest verification to fail when the path\n        # or query string contains percent-encoded reserved characters.\n        path = URL(url).raw_path_qs\n","sourceCodeStart":240,"sourceCodeEnd":276,"githubUrl":"https://github.com/aio-libs/aiohttp/blob/d041d4d0fd48c3f0832084d33be16cf1c4835f85/aiohttp/client_middleware_digest_auth.py#L240-L276","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"# before — server sends: WWW-Authenticate: Digest realm=\"x\", nonce=\"\"\nawait session.get('https://srv/secret')\n\n# after — server must send a populated nonce\n# WWW-Authenticate: Digest realm=\"x\", nonce=\"OA9BSiR4b...\"","handlingStrategy":"try-catch","validationCode":"import re\n\ndef nonce_is_safe(www_authenticate: str) -> bool:\n    m = re.search(r'nonce=\"?([^\",\\s]+)\"?', www_authenticate)\n    return bool(m and m.group(1))","typeGuard":"def nonce_nonempty(challenge_header: str) -> bool:\n    import re\n    m = re.search(r'nonce=\"?([^\"\\s,]*)\"?', challenge_header, re.I)\n    return m is not None and len(m.group(1)) > 0","tryCatchPattern":"from aiohttp import ClientError\n\ntry:\n    resp = await session.get(url)\nexcept ClientError as e:\n    if 'empty' in str(e) and 'nonce' in str(e):\n        # security-sensitive: do not weaken; report to server owner\n        raise SecurityWarning('Server issued empty Digest nonce')\n    raise","preventionTips":["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."],"tags":["digest-auth","authentication","security","http","client-middleware"],"backgroundTag":null,"analyzedSha":"d041d4d0fd48c3f0832084d33be16cf1c4835f85","analyzedAt":"2026-08-11T20:44:15.550Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}