Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,24 @@ Unreleased

- Drop support for Python 3.8 and 3.9.
- Remove previously deprecated code.
- Added warning that itsdangerous signs but does not encrypt data: the
payload is base64-encoded but readable by anyone who holds the token.
Sensitive data should not be placed in tokens without separate encryption.
Added the same note to ``README.md``.
- Documented that :exc:`~itsdangerous.exc.SignatureExpired` is raised
immediately during key rotation verification and does not fall through
to additional keys. Added ``test_secret_keys_expired`` to verify this.
- Added guidance on replacing the full secret key for immediate token
invalidation versus appending to the rotation list for graceful
rollover.
- Added security note that signed tokens are credentials and should not
be logged or exposed to unintended parties.
- Documented that ``key_derivation="none"`` on
:class:`~itsdangerous.signer.Signer` silently ignores the salt, defeating
context separation. Its use is discouraged.
- Added exception-handling guidance covering the ``BadData`` exception
hierarchy, safe catch-all patterns, and pitfalls such as exposing
exception payloads to users or catching exceptions too broadly.


Version 2.2.0
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@

Various helpers to pass data to untrusted environments and to get it
back safe and sound. Data is cryptographically signed to ensure that a
token has not been tampered with.
token has not been tampered with. Signing is not encryption: the payload
is readable by anyone who holds the token; only its integrity is protected.

It's possible to customize how data is serialized. Data is compressed as
needed. A timestamp can be added and verified automatically while
Expand Down
94 changes: 94 additions & 0 deletions docs/concepts.rst
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,26 @@ One way to generate a key is to use :func:`os.urandom`.

$ python3 -c 'import os; print(os.urandom(16).hex())'

.. warning::

itsdangerous **signs** data but does not encrypt it. The payload is
encoded (base64) but not secret — anyone who receives a token can
decode and read its contents. Do not place sensitive data such as
passwords or personally identifiable information inside a token unless
you also encrypt it separately.

If you suspect your secret key has been compromised, replace it with a
single new key rather than appending to the rotation list. Passing a
list allows tokens signed by older keys to remain valid during the
overlap window — that is intentional for graceful rollover, but is the
wrong choice when you need to invalidate all existing tokens immediately.

Signed tokens are credentials: anyone who possesses a token can present
it for the lifetime of the signing key. Do not log token values, include
them in error responses, or expose them in places where unintended
parties can read them (for example, server-side access logs that capture
full URLs will capture any token embedded in a query string).


The Salt
--------
Expand Down Expand Up @@ -97,6 +117,21 @@ Only the serializer with the same salt can load the data.
s2.loads(s2.dumps(42))
42

.. warning::

The salt is a **static, deterministic context label** — it is not
per-token randomness and does not behave like a nonce or IV.
Its sole purpose is to namespace signing keys so that tokens
issued in one context cannot be replayed in another.

Setting ``key_derivation="none"`` on a :class:`~itsdangerous.signer.Signer`
disables key derivation entirely: the raw secret key is used as-is and the
salt is **completely ignored**. Two signers that share the same secret key
but use different salts will produce identical signatures, making the
context-separation guarantee described above worthless. This mode is
discouraged; use the default ``django-concat`` mode unless you have a
specific reason to override it.


Key Rotation
------------
Expand Down Expand Up @@ -133,6 +168,65 @@ a validation error.
s = Serializer(SECRET_KEYS)
s.loads(t) # valid even though it was signed with a previous key

Note that :exc:`~itsdangerous.exc.SignatureExpired` is raised immediately
regardless of how many keys are in the rotation list. If a token's
timestamp has expired under the newest key, it is expired under every
key — itsdangerous does not try additional keys after a timeout failure.
Key rotation does not extend a token's lifetime.


.. _handling-exceptions:

Handling Exceptions
-------------------

All exceptions raised by itsdangerous inherit from
:exc:`~itsdangerous.exc.BadData`, which is the safe catch-all for any
verification failure. The hierarchy is:

- :exc:`~itsdangerous.exc.BadData` — base class for all failures
- :exc:`~itsdangerous.exc.BadSignature` — signature missing or does
not match; subclass of ``BadData``; carries ``.payload``
- :exc:`~itsdangerous.exc.BadTimeSignature` — timestamp is missing or
malformed; subclass of ``BadSignature``; carries ``.payload`` and
``.date_signed``
- :exc:`~itsdangerous.exc.SignatureExpired` — timestamp exceeded
``max_age``; subclass of ``BadTimeSignature``
- :exc:`~itsdangerous.exc.BadHeader` — signed header is invalid;
subclass of ``BadSignature``; carries ``.payload``, ``.header``, and
``.original_error``
- :exc:`~itsdangerous.exc.BadPayload` — payload cannot be
deserialized; subclass of ``BadData``; carries ``.original_error``

Catch :exc:`~itsdangerous.exc.BadData` (or a more specific subclass) and
treat every failure as an untrusted or expired token:

.. code-block:: python

from itsdangerous import BadData

try:
data = s.loads(token)
except BadData:
# Treat as invalid: redirect to login, return 400/403, etc.
...

A few pitfalls to avoid:

- **Do not surface exception details to users.**
:exc:`~itsdangerous.exc.BadSignature` and its subclasses carry a
``payload`` attribute with the unverified decoded data;
:exc:`~itsdangerous.exc.BadHeader` and
:exc:`~itsdangerous.exc.BadPayload` also carry an ``original_error``
attribute. Neither should be forwarded to the client or included in
HTTP responses.
- **Do not catch ``Exception`` broadly.** Swallowing ``BadData`` with a
bare ``except Exception: pass`` silently turns a failed security check
into a no-op.
- **``BadTimeSignature.date_signed``** is available even when the
signature is otherwise intact. It is useful for logging, but it comes
from the token itself and must not be trusted for security decisions.


Digest Method Security
----------------------
Expand Down
6 changes: 6 additions & 0 deletions docs/exceptions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
Exceptions
==========

.. note::

For guidance on catching exceptions safely — including the exception
hierarchy and pitfalls to avoid — see the
:ref:`handling-exceptions` section in :doc:`/concepts`.

.. autoexception:: BadData
:members:

Expand Down
12 changes: 12 additions & 0 deletions tests/test_itsdangerous/test_timed.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,18 @@ def test_sig_error_date_signed(self, signer):

assert isinstance(exc_info.value.date_signed, datetime)

def test_secret_keys_expired(self, ts, freeze):
# Token signed with old key is expired even when rotation list includes old key.
old_signer = TimestampSigner("old-key")
signed = old_signer.sign("value")
freeze.tick(timedelta(seconds=20))
rotation_signer = TimestampSigner(["old-key", "new-key"])

with pytest.raises(SignatureExpired) as exc_info:
rotation_signer.unsign(signed, max_age=10)

assert exc_info.value.date_signed == ts


class TestTimedSerializer(FreezeMixin, TestSerializer):
@pytest.fixture()
Expand Down