English | Русский

Блог о Linux и велосипедах

Предотвращение атак с подменой 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, скорее всего, будет подходящем именем.

Комментарии

RSS
Здесь можно Markdown