{"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/c0ef574e29109210e96e652771ae4e7b88615fa4/aiohttp/client_middleware_digest_auth.py#L240-L276","documentation":"Raised as a ClientError when a server's HTTP Digest auth challenge returns a 'nonce' parameter that is present but empty (e.g. `nonce=\"\"`). The nonce is the security-critical value used to prevent replay attacks in RFC 7616; an empty nonce makes the digest response trivially replayable, so aiohttp refuses to proceed rather than silently produce a weak credential. Unlike a missing realm/nonce (a malformed challenge), this is flagged specifically as a security issue.","triggerScenarios":"The error fires in the digest-auth middleware during challenge parsing (line 257-260), after 'nonce' is confirmed present (line 247) but `not nonce` evaluates True (line 257). It triggers on any 401 response whose WWW-Authenticate digest header contains `nonce=\"\"`.","commonSituations":"Hitting a misconfigured reverse proxy, load balancer, or custom auth filter that emits a Digest challenge with an empty nonce; old/buggy server-side nonce generators; man-in-the-middle appliances that strip nonce values.","solutions":["Report the bug to the server/proxy operator: their Digest challenge is producing an empty nonce, which is a server-side security defect.","If you cannot fix the server, disable digest auth for this endpoint and use a different auth mechanism (e.g. bearer token, basic auth over TLS).","Verify with curl --digest to confirm the server itself is the source of the empty nonce.","Do not attempt to patch the nonce client-side; an empty nonce defeats replay protection."],"exampleFix":"// before: server returns WWW-Authenticate: Digest realm=\"x\", nonce=\"\", qop=\"auth\"\nresp = await session.get(url, auth=DigestAuth(login, pass))\n\n// after: fix server nonce generation, or switch auth\nresp = await session.get(url, headers={\"Authorization\": \"Bearer <token>\"})","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"from aiohttp import ClientError\ntry:\n    await session.get(url, auth=DigestAuth(login, pwd))\nexcept ClientError as e:\n    if \"empty 'nonce'\" in str(e):\n        # server bug; fall back to a different auth method\n        ...","preventionTips":["Do not treat empty-nonce servers as compatible; lobby the operator to fix nonce generation.","When integrating with unknown Digest servers, probe with curl --digest first.","Wrap digest-auth requests in a ClientError handler that logs the raw WWW-Authenticate header."],"tags":["security","auth","digest-auth","http-client"],"analyzedSha":"c0ef574e29109210e96e652771ae4e7b88615fa4","analyzedAt":"2026-08-04T19:51:05.467Z","schemaVersion":2}