Django: Генерируем безопасные отчёты об ошибках на сайте

от автора

Как известно, в Django предусмотрен очень лёгкий и простой механизм уведомления разработчиков о возникающих проблемах. Когда проект развёрнут на локальном компьютере и в настройках DEBUG имеет значение True, то отчёты об ошибках просто выводятся в виде HTTP-ответа, в виде удобной страницы с возможностью копирования traceback’а.

Если же это production-сервер, и DEBUG имеет значение False, то отчёты по умолчанию отправляются по электронной почте всем, кто указан в настройке ADMINS (кстати, если вы используете SMTP-сервер, то письма могут не приходить, так как SMTP-сервер не принимает адрес root@localhost — в этом случае просто укажите любой другой адрес с помощью настройки 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 not 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)) 

И укажем в настройках, что именно этот класс необходимо использовать для фильтрации.

Ну что, теперь стало намного лучше:

 COOKIES:{'csrftoken': 'secret',          'sessionid': 'secret',          'timezone': 'secret'} 

Тем не менее, значения cookies всё ещё присутствуют в словаре META — META[«CSRF_COOKIE»] и META[«HTTP_COOKIE»]. Ну, давайте уберём их и оттуда. 🙂

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, испытывая при этом радость — осознавая, что пользователи теперь в большей безопасности.

ссылка на оригинал статьи http://habrahabr.ru/post/164935/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *