Предотвращение атак с подменой HTTP-заголовка Host
Об атаке с подменой Host
Тот, кто мигрировал с Django 1.4 на 1.5 должен был заметить, что в новой версии
Django настройка ALLOWED_HOSTS
, которая ограничивает работу сайта на заданных
хостах, стала обязательной. В качестве причины такого нововведения упоминается
мера безопасности, которая призвана предотвратить такие атаки, как отравление
кеша и отправка email'ов с ссылками на вредоносные сайты.
Лично мне было далеко не очевидно, как такие атаки осуществимы. Разъяснения я нашёл в статье "Practical HTTP Host header attacks", автор которой создал множество отчётов об ошибках, связанных с данной атакой, для различных веб-фреймворков. Я не стану пересказывать текст статьи, которую я настоятельно рекомендую прочесть, а просто обозначу основную идею.
Основной причиной возможности подобных атак является излишнее доверие со стороны
веб-фреймворков к HTTP-заголовку Host
, которое контролируется на стороне
клиента. Веб-фреймворки часто использует это значение для формирования
абсолютного URI, которое затем встраивается в тело страницы или email. Не
улучшают ситуацию и стандартные настройки веб-серверов. Так, например, Apache
по умолчанию в переменной SERVER_NAME
передаёт веб-приложению значение,
контролируемое клиентом.
Для атаки с подменой Host
используются разные трюки. Одним из них является
указание сразу нескольких заголовков Host
с разными значениями и использование
того факта, что веб-сервер и система кеширования могут взять разные значения.
Другой способ атаки возможен из-за недостаточно надёжной проверки допустимого
значения Host
. Так, через двоеточие в заголовке разрешается указывать порт, но
вместо порта злоумышленник может вставить свой домен, например
Host: example.com:@attacker.com
. Это приведёт к формированию URI в формате с
указанием имени и пароля через двоеточие, но сама ссылка уже будет указывать на
домен злоумышленника: http://example.com:@attacker.com
. Но пожалуй, самым
неожиданным фактом, позволяющим провести атаку, является указание в запросе
абсолютного пути ресурса. Дело в том, что согласно протоколу HTTP/1.1 при
указании абсолютного пути ресурса значение заголовка Host
игнорируется, а в
качестве него берётся хост из пути ресурса. Это приводит к тому, что даже
безопасно настроенный веб-сервер в таком случае принимает запрос с подменённым
значением Host
, и то веб-приложение, которое использует HOST
вместо
SERVER_NAME
подвержено данной атаке.
Ситуация в Django
Обязательно указание допустимых хостов в ALLOWED_HOSTS
в добавок к ранее
реализованной валидации заголовка Host
призвано предотвратить
вышеперечисленные атаки, даже при использовании небезопасно настроенного
веб-сервера.
А побудило меня на это мини-исследование следующее. Когда я обновился с Django
1.4 на 1.5, мне начали приходить письма... Дело в том, что Django 1.5, когда не
находит заголовок Host
в списке допустимых значений, выбрасывает исключение
SuspiciousOperation, которое приводит к ошибке 500 (внутренняя ошибка
сервера), которая в свою очередь, как правило, приводит к отправке письма на
адрес администратора.
То, что подозрительное действие трактуется как ошибка сервера, несомненно является багом Django 1.5. В баг-трекере Django есть соответствующий дефект, который уже исправлен и должен попасть в Django 1.6. С версии 1.6 в таких случаях в качестве ответа будет приходить ошибка 400 (неверный запрос), и для большинства конфигураций не будет приводить к отправке писем администратору.
Автор дефекта даже написал о том, как обойти проблему отправки писем. Суть
метода заключается в создании дополнительного фильтра для логирования, который
не пропускает исключение SuspiciousOperation, и который устанавливается для
handler'а mail_admins
. Данное решение не исправляет саму проблему: приложение
по-прежнему отвечает ошибкой 500 на запросы с неправильными хостами.
Надо сказать, что в моём случае причиной подозрительных действий была вовсе не
подмена злоумышленниками заголовка Host
, а всего лишь тот факт, что
виртуальный сервер был также доступен по доменному имени, автоматически
созданным провайдером, обращение по которому было нежелательно. Описание
конфигурации Apache в следующем разделе подходит для обоих случаев: и для
предотвращения атак с подменой заголовком Host
и для запрета обращения к сайту
по определённому домену.
Конфигурация Apache
Если сервер находится в нашем управлении, то можно сделать так, чтобы запросы с неизвестными хостами вообще не доходили до веб-приложения, а обрабатывались непосредственно веб-сервером. Это и снижает нагрузку с веб-приложения, и избавляет от необходимости использовать обходные пути в Django.
Для начала необходимо выяснить, каким образом веб-сервер (Apache) определяет,
какому приложению (wsgi-приложению в случае с Django) передать управление. Как
известно, в HTTP/1.1 на одной машине с одним IP-адресом могут работать несколько
веб-приложений. Веб-сервер определяет, какому из приложений передать управление
по значению HTTP-заголовка Host
. Для этого в конфигурации Apache существуют
директивы ServerName
и ServerAlias
, определяемые внутри секции
<VirtualHost>
. Apache пробегает по значениям этих директив и в случае
совпадения, передаёт управление приложению, описанному в этой секции
<VirtualHost>
. Самое интересное происходит тогда, когда Apache не находит ни
одного подходящего имени. В этом случае Apache использует первую
определённую в конфигурационном файле секцию <VirtualHost>
.
Так, даже в случае с одним виртуальным хостом, определённое в нём веб-приложение
будет обрабатывать все запросы, независимо от того, совпадает ли его имя с
заголовком Host
или нет. Это и есть та причина, по которой моё приложение
выполнялось и слало мне письма, когда использовались неизвестные (как серверу,
так и Django-приложению) хосты. Очевидным решением проблемы является создание
ещё одной секции, определяющей виртуальный хост, которая бы служила своего рода
фильтром — обрабатывала все запросы с неопределёнными хостами. Для этого секцию
необходимо разместить самой первой в конфигурационном файле Apache. Вот пример:
<VirtualHost *:80> ServerName arbiraty-domain.org ServerAdmin admin@example.com RedirectMatch gone .* </VirtualHost>
Здесь в директиве RedirectMatch
указывается, что на любой путь (.*
) должен
выдаваться ответ с кодом 410
(gone
). Эта ошибка отличается от 404
тем, что
предполагается, что ресурс не появится в будущем и не стоит проверять его
доступность через время. Думаю, в данном случае подходящими также будут коды
ошибок 404
и 400
. Значение ServerName
здесь роли не играет — главное,
чтобы данный виртуальный хост был указан первым в конфигурации.
Ремарка к Apache в Debian
В Debian и производных дистрибутивах поставляется модифицированный
конфигурационный файл apache2.conf
. Из этого файла включается файлы из
sites-enabled
, в который они попадают путём создания символических ссылок из
sites-available
командой a2ensite
. Директива Include
из apache2.conf
включает файлы из папки в лексографическом порядке, поэтому важно файлу дать
такое имя, чтобы он всегда включалось первым. Например, 00-default, скорее
всего, будет подходящем именем.
Комментарии