Как известно, в Django предусмотрен очень лёгкий и простой механизм уведомления разработчиков о возникающих проблемах. Когда проект развёрнут на локальном компьютере и в настройках DEBUG имеет значение True, то отчёты об ошибках просто выводятся в виде HTTP-ответа, в виде удобной страницы с возможностью копирования traceback'а.
Если же это production-сервер, и DEBUG имеет значение False, то отчёты по умолчанию отправляются по электронной почте всем, кто указан в настройке ADMINS (кстати, если вы используете SMTP-сервер, то письма могут не приходить, так как SMTP-сервер не принимает адрес root@localhost — в этом случае просто укажите любой другой адрес, который будет принимать ваш SMTP-сервер, с помощью настройки SERVER_EMAIL).
Разумеется, ничего не мешает также написать свой logging handler (обработчик журналирования) и сохранять отчёты об ошибках в любом нужном виде — создавать задачу в баг-трекере, например.
Тем не менее, если для вас важна безопасность ваших пользователей, то возникает вполне закономерный вопрос — как сделать так, чтобы отчёты об ошибках были для них безопасны? То есть как сделать, чтобы никакая личная информация в них не сохранялась, и не отправлялась кому-либо по почте (ведь дело даже не в том, что кто-то из разработчиков может вести себя недобросовестно, а скорее в том, что подобную информацию вообще лучше не сохранять где-либо за пределами сервера — ведь почтовый ящик и взломать могут, а сервер обычно защищён лучше).
На самом деле эта проблема очень легко решается в Django, и решение почти целиком описано в секции «How-to» официальной документации.
Для примера возьмём простое представление (view) для авторизации:
from django.http import HttpResponse, HttpResponseRedirect
from django.core.urlresolvers import reverse
from django.contrib.auth import authenticate, login
def login_view(request):
if request.method != "POST":
return HttpResponse("Please use POST.")
user = authenticate(
email=request.POST.get("email"),
password=request.POST.get("password")
)
if user is not None:
if user.is_active:
login(request, user)
status = "ok"
else:
status = "account_disabled"
else:
status = "invalid_credentials"
if status != "ok":
return HttpResponse(status)
return HttpResponseRedirect(reverse('app.views.index'))
Если будете тестировать у себя, то не забудьте либо поменять email на username, либо добавить авторизационный бэкэнд для входа с помощью адреса электронной почты:
from django.contrib.auth.backends import ModelBackend
from django.contrib.admin.models import User
class EmailAuthBackend(ModelBackend):
def authenticate(self, email=None, password=None, **kwargs):
try:
user = User.objects.get(email=email)
except User.DoesNotExist:
return None
except User.MultipleObjectsReturned:
user = User.objects.filter(email=email)[0]
if user.check_password(password):
return user
return None
А теперь посмотрим, что будет при возникновении какой-либо ошибки. На практике ошибка тут скорее всего может быть связана с недоступностью базы данных, но для теста можно просто добавить в начале функции вызов исключения:
raise Exception
Теперь, если кто-то попытается залогиниться, то по почте приходит отчёт, содержащий, в частности, информацию обо всех POST-параметрах запроса:
POST:<QueryDict: {u'csrfmiddlewaretoken': [u'F3d71EHWECfavaeK4H7nUTzLwgY07AHT'], u'password': [u'123'], u'email': [u'aruseni.magiku@gmail.com']}>
Ну, что ж, вот и email, и пароль. А теперь попробуем обернуть функцию в декоратор sensitive_post_parameters:
…
from django.views.decorators.debug import sensitive_post_parameters
@sensitive_post_parameters("password")
def login_view(request):
…
Неплохо, теперь вместо пароля в отчёт включены 20 звёздочек (********************):
POST:<QueryDict: {u'csrfmiddlewaretoken': [u'F3d71EHWECfavaeK4H7nUTzLwgY07AHT'], u'password': [u'********************'], u'email': [u'aruseni.magiku@gmail.com']}>
Кстати, декоратор sensitive_post_parameters может принимать сразу несколько аргументов (в зависимости от того, значения скольки POST-параметров вы хотите скрыть в отчёте). А можно вообще не указывать аргументы:
…
from django.views.decorators.debug import sensitive_post_parameters
@sensitive_post_parameters()
def login_view(request):
…
В этом случае в отчёте об ошибке оказываются скрыты значения вообще всех POST-параметров:
POST:<QueryDict: {u'csrfmiddlewaretoken': [u'********************'], u'password': [u'********************'], u'email': [u'********************']}>
Но личная информация, возможности раскрытия которой необходимо предотвратить, может содержаться не только в POST-параметрах, переданных в запросе, но и, например, в локальных переменных, которые определяет функция (и значения которых включаются в отчёты об ошибках). Представьте, к примеру, что у вас есть функция process_payment, которая, в частности, получает из базы данных номер банковской карты пользователя и записывает его в локальную переменную payment_card_id. Очевидно, что значение этой переменной нужно обязательно скрыть в отчёте об ошибке.
Сделать это можно с помощью декоратора sensitive_variables:
…
from django.views.decorators.debug import sensitive_variables
@sensitive_variables("payment_card_id")
def process_payment(request):
…
Как и sensitive_post_parameters, sensitive_variables поддерживает использование нескольких аргументов (для того, чтобы скрыть большее количество переменных), а также использование без каких-либо аргументов (для скрытия всех локальных переменных функции).
Тем не менее, всё ещё остаётся некоторая деликатная информация — cookies. Которые, в частности, содержат идентификатор сессии (а возможный перехват идентификатора сессии это очень нехорошо).
COOKIES:{'csrftoken': 'F3d71EHWECfavaeK4H7nUTzLwgY07AHT', 'sessionid': '262661787a7f42e787ad18ee853ef8d6'}
Что ж, это немного сложнее, но незначительно.
Добавим в приложение (здесь оно называется «app», у вас может быть как-то иначе) файл debug.py и добавим туда собственный класс фильтрации отчётов об ошибках (он будет наследоваться от класса SafeExceptionReporterFilter, который используется для фильтрации в тех случаях, когда были использованы декораторы sensitive_post_parameters и sensitive_variables):
from django.views.debug import SafeExceptionReporterFilter
from django.http import build_request_repr
class CustomExceptionReporterFilter(SafeExceptionReporterFilter):
def get_cookies(self, request):
if request is None:
return {}
else:
cleansed = request.COOKIES.copy()
for key, value in cleansed.iteritems():
cleansed[key] = "secret"
return cleansed
def get_request_repr(self, request):
if request is None:
return repr(None)
else:
return build_request_repr(request, POST_override=self.get_post_parameters(request), COOKIES_override=self.get_cookies(request))
И укажем в настройках, что именно этот класс необходимо использовать для фильтрации:
DEFAULT_EXCEPTION_REPORTER_FILTER = 'app.views.CustomExceptionReporterFilter'
Ну что, теперь стало намного лучше:
COOKIES:{'csrftoken': 'secret', 'sessionid': 'secret', 'timezone': 'secret'}
Тем не менее, значения cookies всё ещё присутствуют в словаре META — META["e;CSRF_COOKIE"e;] и META["e;HTTP_COOKIE"e;]. Ну, давайте уберём их и оттуда. :)
from django.views.debug import SafeExceptionReporterFilter
from django.http import build_request_repr
class CustomExceptionReporterFilter(SafeExceptionReporterFilter):
def get_cookies(self, request):
if request is None:
return {}
else:
cleansed = request.COOKIES.copy()
for key, value in cleansed.iteritems():
cleansed[key] = "secret"
return cleansed
def get_meta(self, request):
if request is None:
return {}
else:
cleansed = request.META.copy()
for key in ("HTTP_COOKIE", "CSRF_COOKIE"):
cleansed[key] = "secret"
return cleansed
def get_request_repr(self, request):
if request is None:
return repr(None)
else:
return build_request_repr(request, POST_override=self.get_post_parameters(request), COOKIES_override=self.get_cookies(request), META_override=self.get_meta(request))
Ну вот, теперь у вас есть уникальная возможность посмотреть на ошибку 500, испытывая при этом радость — осознавая, что пользователи теперь в большей безопасности.
Автор: MaGIc2laNTern