{"record":{"id":"6cb82e69344efd7b","repo":"django/django","slug":"signature-s-does-not-match","errorCode":null,"errorMessage":"Signature \"%s\" does not match","messagePattern":"Signature \"(.+?)\" does not match","errorType":"exception","errorClass":"BadSignature","httpStatus":null,"severity":"error","filePath":"django/core/signing.py","lineNumber":254,"sourceCode":"                \"Unsafe Signer separator: %r (cannot be empty or consist of \"\n                \"only A-z0-9-_=)\" % sep,\n            )\n\n    def signature(self, value, key=None):\n        key = key or self.key\n        return base64_hmac(self.salt + \"signer\", value, key, algorithm=self.algorithm)\n\n    def sign(self, value):\n        return \"%s%s%s\" % (value, self.sep, self.signature(value))\n\n    def unsign(self, signed_value):\n        if self.sep not in signed_value:\n            raise BadSignature('No \"%s\" found in value' % self.sep)\n        value, sig = signed_value.rsplit(self.sep, 1)\n        for key in [self.key, *self.fallback_keys]:\n            if constant_time_compare(sig, self.signature(value, key)):\n                return value\n        raise BadSignature('Signature \"%s\" does not match' % sig)\n\n    def sign_object(self, obj, serializer=JSONSerializer, compress=False):\n        \"\"\"\n        Return URL-safe, hmac signed base64 compressed JSON string.\n\n        If compress is True (not the default), check if compressing using zlib\n        can save some space. Prepend a '.' to signify compression. This is\n        included in the signature, to protect against zip bombs.\n\n        The serializer is expected to return a bytestring.\n        \"\"\"\n        data = serializer().dumps(obj)\n        # Flag for if it's been compressed or not.\n        is_compressed = False\n\n        if compress:\n            # Avoid zlib dependency unless compress is being used.\n            compressed = zlib.compress(data)","sourceCodeStart":236,"sourceCodeEnd":272,"githubUrl":"https://github.com/django/django/blob/b5388a3a80cafcce2e34196d8e81cf5b48eb33bb/django/core/signing.py#L236-L272","documentation":"`Signer.unsign()` (signing.py:250-254) raises `BadSignature('Signature \"%s\" does not match' % sig)` after splitting the value on the separator and finding that the supplied signature fails constant-time comparison against the recomputed HMAC for every candidate key (`self.key` plus each `fallback_keys`). This is the canonical tamper-detection signal for Django's signed tokens.","triggerScenarios":"Calling `signer.unsign(token)` or `signing.loads(token)` where the value portion was altered after signing (so the appended HMAC no longer matches), or the token was signed with a different `SECRET_KEY`/salt than the verifier uses. The loop at lines 251-252 tries the primary key and all `fallback_keys`; if none match, it raises.","commonSituations":"User tampering with a signed cookie or activation link; rotating `SECRET_KEY` (or changing the `salt`) without keeping the old key in `SECRET_KEY_FALLBACKS`; copying a token between environments with different secrets; clock/key drift causing `TimestampSigner` to recompute a different HMAC.","solutions":["If you recently rotated `SECRET_KEY`, add the previous value(s) to `SECRET_KEY_FALLBACKS` so old tokens still verify.","Confirm the `salt` passed to `dumps`/`loads` (or the `Signer`) is identical on both sides; the default salt is `'django.core.signing'`.","Treat the raise as expected for untrusted input: catch `BadSignature` (and its `SignatureExpired` subclass) and return a graceful 'invalid/expired' response.","Regenerate the token from a trusted source if legitimate data failed due to tampering or copy errors."],"exampleFix":"// before\ntry:\n    data = signing.loads(token)\nexcept signing.BadSignature:\n    return HttpResponse('Bad token', status=400)  # happens at signing.py:254\n// after (support key rotation)\ntry:\n    data = signing.loads(token, max_age=3600)\nexcept signing.SignatureExpired:\n    return HttpResponse('expired', status=400)\nexcept signing.BadSignature:\n    return HttpResponse('invalid signature', status=400)","handlingStrategy":"try-catch","validationCode":"from django.conf import settings\nfrom django.core.signing import Signer\n\ndef can_verify_with_current_keys(token: str) -> bool:\n    signer = Signer()\n    for key in [settings.SECRET_KEY, *settings.SECRET_KEY_FALLBACKS]:\n        try:\n            signer.unsign(token)\n            return True\n        except Exception:\n            continue\n    return False","typeGuard":null,"tryCatchPattern":"from django.core import signing\ntry:\n    data = signing.loads(token, max_age=3600)\nexcept signing.SignatureExpired:\n    return 'expired', None\nexcept signing.BadSignature:\n    return 'invalid', None\nelse:\n    return 'ok', data","preventionTips":["When rotating SECRET_KEY, keep prior values in SECRET_KEY_FALLBACKS until tokens expire.","Use identical salt values for dumps and loads (default is 'django.core.signing').","Always handle both BadSignature and SignatureExpired for signed, time-stamped tokens."],"tags":["signing","bad-signature","security","secret-key","tokens","hmac"],"backgroundTag":null,"analyzedSha":"b5388a3a80cafcce2e34196d8e81cf5b48eb33bb","analyzedAt":"2026-08-10T17:37:52.993Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}