Spec-Zone.ru › Django 6.0

Криптографическая подпись

Золотое правило безопасности веб-приложений — никогда не доверять данным из ненадёжных источников. Иногда бывает полезно передавать данные через ненадёжную среду. Криптографически подписанные значения можно передавать по ненадёжному каналу, будучи уверенным, что любые попытки их изменить будут обнаружены.

Django предоставляет низкоуровневый API для подписания значений и высокоуровневый API для установки и чтения подписанных файлов cookie — одного из наиболее распространённых способов применения подписей в веб-приложениях.

Подписание также может пригодиться в следующих случаях:

  • Создание URL-адресов для восстановления учётной записи, которые можно отправить пользователям, забывшим пароль.
  • Проверка того, что данные в скрытых полях формы не были изменены.
  • Создание одноразовых секретных URL-адресов для временного доступа к защищённому ресурсу, например к загружаемому файлу, за который пользователь заплатил.

Защита SECRET_KEY и резервные SECRET_KEY_FALLBACKS

При создании нового проекта Django с помощью startproject файл settings.py создаётся автоматически и получает случайное значение SECRET_KEY. Это значение служит ключом для защиты подписанных данных: крайне важно хранить его в безопасности, иначе злоумышленники смогут создавать собственные подписанные значения.

SECRET_KEY_FALLBACKS можно использовать для смены секретных ключей. Эти значения не используются для подписания данных, но, если они заданы, используются для проверки подписанных данных и должны храниться в безопасности.

Использование низкоуровневого API

Методы подписания Django находятся в модуле django.core.signing. Чтобы подписать значение, сначала создайте экземпляр Signer:

>>> from django.core.signing import Signer
>>> signer = Signer()
>>> value = signer.sign("My string")
>>> value
'My string:v9G-nxfz3iQGTXrePqYPlGvH79WTcIgj1QIQSUODTW0'

Подпись добавляется в конец строки после двоеточия. Исходное значение можно получить с помощью метода unsign:

>>> original = signer.unsign(value)
>>> original
'My string'

Если передать в sign значение нестрокового типа, перед подписанием оно будет преобразовано в строку, и результат unsign вернёт эту строку:

>>> signed = signer.sign(2.5)
>>> original = signer.unsign(signed)
>>> original
'2.5'

Для защиты списка, кортежа или словаря можно использовать методы sign_object() и unsign_object():

>>> signed_obj = signer.sign_object({"message": "Hello!"})
>>> signed_obj
'eyJtZXNzYWdlIjoiSGVsbG8hIn0:bzb48DBkB-bwLaCnUVB75r5VAPUEpzWJPrTb80JMIXM'
>>> obj = signer.unsign_object(signed_obj)
>>> obj
{'message': 'Hello!'}

Подробнее см. в разделе Защита сложных структур данных.

Если подпись или значение были каким-либо образом изменены, будет вызвано исключение django.core.signing.BadSignature:

>>> from django.core import signing
>>> value += "m"
>>> try:
...     original = signer.unsign(value)
... except signing.BadSignature:
...     print("Tampering detected!")
...

По умолчанию класс Signer использует настройку SECRET_KEY для создания подписей. Можно указать другой секрет, передав его конструктору Signer:

>>> signer = Signer(key="my-other-secret")
>>> value = signer.sign("My string")
>>> value
'My string:o3DrrsT6JRB73t-HDymfDNbTSxfMlom2d8TiUlb1hWY'
class Signer(*, key=None, sep=':', salt=None, algorithm=None, fallback_keys=None) [исходный код]

Возвращает объект-подписант, который использует key для создания подписей и sep для разделения значений. sep не может входить в алфавит URL-безопасного base64. Этот алфавит содержит буквенно-цифровые символы, дефисы и подчёркивания. algorithm должен быть алгоритмом, поддерживаемым hashlib; по умолчанию используется 'sha256'. fallback_keys — список дополнительных значений, используемых для проверки подписанных данных; по умолчанию используется SECRET_KEY_FALLBACKS.

Использование аргумента salt

Если вы не хотите, чтобы каждое вхождение определённой строки имело одинаковую хеш-подпись, можно использовать необязательный аргумент salt класса Signer. Соль инициализирует функцию хеширования подписи, используя как соль, так и ваш SECRET_KEY:

>>> signer = Signer()
>>> signer.sign("My string")
'My string:v9G-nxfz3iQGTXrePqYPlGvH79WTcIgj1QIQSUODTW0'
>>> signer.sign_object({"message": "Hello!"})
'eyJtZXNzYWdlIjoiSGVsbG8hIn0:bzb48DBkB-bwLaCnUVB75r5VAPUEpzWJPrTb80JMIXM'
>>> signer = Signer(salt="extra")
>>> signer.sign("My string")
'My string:YMD-FR6rof3heDkFRffdmG4pXbAZSOtb-aQxg3vmmfc'
>>> signer.unsign("My string:YMD-FR6rof3heDkFRffdmG4pXbAZSOtb-aQxg3vmmfc")
'My string'
>>> signer.sign_object({"message": "Hello!"})
'eyJtZXNzYWdlIjoiSGVsbG8hIn0:-UWSLCE-oUAHzhkHviYz3SOZYBjFKllEOyVZNuUtM-I'
>>> signer.unsign_object(
...     "eyJtZXNzYWdlIjoiSGVsbG8hIn0:-UWSLCE-oUAHzhkHviYz3SOZYBjFKllEOyVZNuUtM-I"
... )
{'message': 'Hello!'}

Использование соли таким образом помещает разные подписи в отдельные пространства имён. Подпись из одного пространства имён (с определённым значением соли) нельзя использовать для проверки той же строки открытого текста в другом пространстве имён, где задано другое значение соли. Это не позволяет злоумышленнику использовать подписанную строку, созданную в одной части кода, в качестве входных данных для другой части кода, которая создаёт и проверяет подписи с другой солью.

В отличие от SECRET_KEY, аргумент соли не обязательно хранить в секрете.

Проверка значений с временной меткой

TimestampSigner — это подкласс Signer, который добавляет к значению подписанную временную метку. Это позволяет убедиться, что подписанное значение было создано в течение заданного периода времени:

>>> from datetime import timedelta
>>> from django.core.signing import TimestampSigner
>>> signer = TimestampSigner()
>>> value = signer.sign("hello")
>>> value
'hello:1stLqR:_rvr4oXCgT4HyfwjXaU39QvTnuNuUthFRCzNOy4Hqt0'
>>> signer.unsign(value)
'hello'
>>> signer.unsign(value, max_age=10)
SignatureExpired: Signature age 15.5289158821 > 10 seconds
>>> signer.unsign(value, max_age=20)
'hello'
>>> signer.unsign(value, max_age=timedelta(seconds=20))
'hello'
class TimestampSigner(*, key=None, sep=':', salt=None, algorithm='sha256') [исходный код]
sign(value) [исходный код]

Подписывает value и добавляет к нему текущую временную метку.

unsign(value, max_age=None) [исходный код]

Проверяет, было ли value подписано менее max_age секунд назад; в противном случае вызывает SignatureExpired. Параметр max_age может принимать целое число или объект datetime.timedelta.

sign_object(obj, serializer=JSONSerializer, compress=False)

Кодирует, при необходимости сжимает, добавляет текущую временную метку и подписывает сложную структуру данных (например, список, кортеж или словарь).

unsign_object(signed_obj, serializer=JSONSerializer, max_age=None)

Проверяет, было ли signed_obj подписано менее max_age секунд назад; в противном случае вызывает SignatureExpired. Параметр max_age может принимать целое число или объект datetime.timedelta.

Защита сложных структур данных

Для защиты списка, кортежа или словаря можно использовать методы Signer.sign_object() и unsign_object() либо функции dumps() или loads() из модуля signing (это сокращённые варианты TimestampSigner(salt='django.core.signing').sign_object()/unsign_object()). Под капотом они используют сериализацию JSON. Благодаря JSON, даже если ваш SECRET_KEY будет украден, злоумышленник не сможет выполнять произвольные команды, используя уязвимости формата pickle:

>>> from django.core import signing
>>> signer = signing.TimestampSigner()
>>> value = signer.sign_object({"foo": "bar"})
>>> value
'eyJmb28iOiJiYXIifQ:1stLrZ:_QiOBHafwucBF9FyAr54qEs84ZO1UdsO1XiTJCvvdno'
>>> signer.unsign_object(value)
{'foo': 'bar'}
>>> value = signing.dumps({"foo": "bar"})
>>> value
'eyJmb28iOiJiYXIifQ:1stLsC:JItq2ZVjmAK6ivrWI-v1Gk1QVf2hOF52oaEqhZHca7I'
>>> signing.loads(value)
{'foo': 'bar'}

В силу особенностей JSON (в нём нет встроенного различия между списками и кортежами), если передать кортеж, signing.loads(object) вернёт список:

>>> from django.core import signing
>>> value = signing.dumps(("a", "b", "c"))
>>> signing.loads(value)
['a', 'b', 'c']
dumps(obj, key=None, salt='django.core.signing', serializer=JSONSerializer, compress=False) [исходный код]

Возвращает URL-безопасную подписанную строку JSON в base64, сжатую с помощью gzip. Сериализованный объект подписывается с использованием TimestampSigner.

loads(string, key=None, salt='django.core.signing', serializer=JSONSerializer, max_age=None, fallback_keys=None) [исходный код]

Выполняет обратное преобразование для dumps() и вызывает BadSignature, если подпись не прошла проверку. При наличии параметра проверяет max_age (в секундах).

© Django Software Foundation and individual contributors
Licensed under the BSD License.
https://docs.djangoproject.com/en/6.0/topics/signing/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API