Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Django_-_podrobnoe_rukovodstvo

.pdf
Скачиваний:
373
Добавлен:
01.03.2016
Размер:
5 Мб
Скачать
☆

20

Безопасность

Интернет временами вселяет страх.

В наши дни чуть ли не ежедневно появляются сообщения о брешах

всистемах­ безопасности. О чем мы только не слышим: о вирусах, распространяющихся с бешеной скоростью; о легионах зараженных компьютеров, используемых как оружие; о нескончаемой войне со спамерами и бесчисленных случаях кражи персональных данных со взломанных веб-сайтов.

Будучи веб-разработчиками, мы обязаны делать все, что в наших силах, для борьбы с темными силами. Любой разработчик должен считать безопасность неотъемлемой частью веб-программирования. К сожалению, обеспечить безопасность трудно – атакующему достаточно найти хотя бы одну уязвимость, а обороняющемуся необходимо защититься от всех.

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

Но сразу же оговоримся: мы не стремились представить исчерпывающее руководство по всем известным уязвимостям и не пытались подробно описать даже те, что упоминаются здесь. Мы лишь дадим краткий обзор проблематики, связанной с безопасностью в контексте Django.

Безопасность в Сети

Самая главная мысль этой главы, которую вы должны запомнить крепко-накрепко:

Внедрение SQL

391

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

Вы не можете знать, кто находится на другом конце HTTP-соединения. Быть может, это добропорядочный пользователь, а быть может, мерзкий взломщик, ищущий дыру в вашем сайте.

К любым данным, пришедшим от броузера, следует относиться со здоровой долей паранойи. Это касается как «основных» данных (переданных в составе формы), так и «сопутствующих» (HTTP-заголовки, cookie и т. п.). Подделать метаданные запроса, которые броузер включает автоматически, – тривиальная задача.

Причина всех обсуждаемых в этой главе уязвимостей одна и та же: чрезмерное доверие к данным, пришедшим по проводам, и пренебрежение их проверкой. Возьмите себе в привычку всегда задаваться вопросом: «Откуда поступили эти данные?».

Внедрение SQL

Внедрение SQL1 – это широко распространенная атака, при проведении которой злоумышленник изменяет параметры веб-страницы (такие как данные в запросе GET/POST или в URL), чтобы ничего не подозревающее приложение выполнило неожиданный SQL-запрос к базе. Пожалуй, это самая опасная и, к сожалению, одна из самых часто встречающихся уязвимостей.

Обычно такая уязвимость возникает, когда SQL-запрос строится вручную с использованием поступивших от пользователя данных. Представим, к примеру, функцию, которая возвращает контактную информацию в ответ на поисковый запрос. Чтобы спамер не мог получить все адреса электронной почты из нашей системы,­ мы требуем ввести в форму имя конкретного пользователя:

def user_contacts(request):

user = request.GET[‘username’]

sql = “SELECT * FROM user_contacts WHERE username = ‘%s’;” % username

# выполнить SQL-запрос...

Примечание

В этом и всех последующих примерах того, как не надо поступать, мы умышленно опускаем код, необходимый для работы функции. Мы не хотим, чтобы кто-нибудь случайно скопировал его в свою программу.

На первый взгляд в этой конструкции нет ничего опасного. Однако это не так. Прежде всего, наша попытка запретить вывод всего списка почтовых адресов оказалась несостоятельна; коварно составленный по-

1Часто этот вид атак называют «инъекция SQL». – Прим. науч. ред.

392

Глава 20. Безопасность

исковый запрос сводит все усилия на нет. Подумайте, что произойдет, если злоумышленник введет в поле запроса строку “’ OR ‘a’=’a”. В данном случае результатом интерполяции станет такой SQL-запрос:

SELECT * FROM user_contacts WHERE username = ‘’ OR ‘a’ = ‘a’;

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

Но это еще меньшее из возможных зол. Представьте, что случится, если злоумышленник введет в поисковое поле строку “’; DELETE FROM user_ contacts WHERE ‘a’ = ‘a”. Тогда получится такой запрос (состоящий из двух SQL-команд):

SELECT * FROM user_contacts WHERE username = ‘’;

DELETE FROM user_contacts WHERE ‘a’ = ‘a’;

О боже! В мгновение ока мы потеряли весь свой список контактов.

Решение

Хотя проблема коварна и не всегда заметна с первого взгляда, решается она просто: никогда не доверять данным, полученным от пользователя, и всегда экранировать их перед вставкой в SQL-запрос.

API доступа к базе данных в Django делает это автоматически: экранирует все параметры SQL-запроса, применяя соглашения об употреблении кавычек, действующие в используемой СУБД (например, PostgreSQL или MySQL).

Например, в следующем вызове API

foo.get_list(bar__exact=”’ OR 1=1”)

Django экранирует входные данные, в результате чего формируется такая команда:

SELECT * FROM foos WHERE bar = ‘\’ OR 1=1’

Вот теперь все безопасно.

Это относится ко всем частям API доступа к базе данных с двумя исключениями:

•• Аргумент where метода extra(). В этом параметре можно передать необработанный SQL-код; так и задумано.

•• Запросы, составленные вручную с помощью низкоуровневого API (см. главу 10).

Но и в этих двух случаях защититься совсем нетрудно. Достаточно просто отказаться от конструирования запросов вручную и использовать параметризованные запросы. Так, пример, с которого мы начали эту главу, следовало бы написать так:

Межсайтовый скриптинг (XSS)

393

from django.db import connection

def user_contacts(request):

user = request.GET[‘username’]

sql = “SELECT * FROM user_contacts WHERE username = %s” cursor = connection.cursor()

cursor.execute(sql, [user])

# ... обработать результаты

Низкоуровневый метод execute принимает строку SQL-запроса с параметрами %s и автоматически подставляет вместо них значения из списка, переданного во втором аргументе, попутно осуществляя экранирование. Именно так следует поступать всякий раз, когда возникает необходимость строить SQL-запрос самостоятельно.

К сожалению, параметры в запросах могут использоваться не везде;

вSQL не разрешается использовать параметры в качестве идентификаторов (то есть имен таблиц и столбцов). Поэтому, если потребуется динамически указать имя таблицы, взяв его из параметров POST-запроса, то экранировать это имя вам придется самостоятельно. Для этой цели Django предлагает функцию django.db.connection.ops.quote_name, которая экранирует идентификаторы, следуя соглашениям, принятым в текущей СУБД.

Межсайтовый скриптинг (XSS)

Атака методом межсайтового скриптинга (cross-site scripting – XSS) возможна в том случае, когда веб-приложение не экранирует полученные от пользователя данные перед вставкой их в HTML-разметку. В результате злоумышленник получает возможность поместить в страницу произвольный HTML-код, обычно тег <script>.

XSS-атаки часто применяются для кражи информации из cookie и сеансов или для того, чтобы обманом «выудить» у пользователя конфиденциальную информацию (фишинг).

Атаки этого типа могут принимать самые разнообразные формы, с почти бесконечным количеством вариаций, поэтому мы рассмотрим лишь наиболее типичный пример. Возьмем тривиальное представление:

from django.http import HttpResponse

def say_hello(request):

name = request.GET.get(‘name’, ‘world’)

return HttpResponse(‘<h1>Hello, %s!</h1>’ % name)

Оно просто читает имя из параметра GET-запроса и вставляет его в HTMLразметку. При обращении по URL http://example.com/hello/?name=Jacob

страница имела бы такой вид:

<h1>Hello, Jacob!</h1>

394

Глава 20. Безопасность

Но не спешите. А что, если пользователь обратится по URL http://example. com/hello/?name=<i>Jacob</i>? Тогда мы получим такую разметку:

<h1>Hello, <i>Jacob</i>!</h1>

Разумеется, злоумышленник не ограничится безобидными тегами <i>; он может включить в URL произвольный HTML-код и сделать с вашей страницей все, что угодно. Такая атака применялась, чтобы заставить пользователя ввести данные на странице, которая выглядит в точности, как веб-страница его банка, но на самом деле содержит поддельную форму, отправляющую данные о банковском счете злоумышленнику.

Ситуация становится еще хуже, когда данные сохраняются в базе, а потом отображаются на сайте. Например, на сайте MySpace в свое время была уязвимость к XSS-атаке такого типа. Пользователь вставил в свой профиль JavaScript-сценарий, который автоматически добавлял его в список друзей любого, кто просматривал его профиль. За считанные дни он обзавелся миллионами друзей.

Может, это кажется вам невинной шалостью, но подумайте, как бы вы отнеслись к тому, что злоумышленник сумел выполнить свой код (не код, написанный программистами MySpace) на вашем компьютере. В таком случае было бы подорвано доверие вообще ко всему коду на сайте MySpace.

Сайту MySpace еще повезло, что этот вредоносный код не удалял автоматически учетные записи пользователей, не изменял их пароли, не затопил сайт спамом и не реализовал еще какой-нибудь кошмарный сценарий – найденная уязвимость открывала для этого все возможности.

Решение

Решение простое: всегда экранируйте любую информацию, пришедшую от пользователя, прежде чем вставлять ее в HTML-разметку.

Для защиты от этой атаки система­ шаблонов­ в Django автоматически экранирует значения всех переменных. Посмотрим, что получится, если переписать приведенный выше пример с использованием системы­ шаблонов:­

# views.py

from django.shortcuts import render_to_response

def say_hello(request):

name = request.GET.get(‘name’, ‘world’)

return render_to_response(‘hello.html’, {‘name’: name})

# hello.html

<h1>Hello, {{ name }}!</h1>

Теперь обращение к URL http://example.com/hello/name=<i>Jacob</i> породит такую страницу:

Подделка HTTP-запросов

395

<h1>Hello, <i>Jacob</i>!</h1>

Мы рассматривали автоматическое экранирование в главе 4, где рассказали также о том, как его отключить. Но даже используя эту возможность, вы все равно должны неизменно задавать себе вопрос: «Откуда поступили эти данные?». Никакое автоматизированное решение не способно защитить от всех XSS-атак.

Подделка HTTP-запросов

Атака путем подделки HTTP-запросов (cross-site request forgery – CSRF) случается, когда вредоносный сайт обманом заставляет ничего не подозревающего пользователя перейти по ссылке с сайта, на котором он уже аутентифицировался, и тем самым воспользоваться его правами.

В Django встроены средства для защиты от атак такого типа. (Сама атака и средства защиты от нее подробно описаны в главе 16.)

Атака на данные сеанса

Это не какая-то конкретная атака, а целый класс атак, направленных на получение данных пользователя, хранящихся в сеансе. Они могут принимать разные формы:

•• Атака с незаконным посредником, когда злоумышленник перехватывает данные сеанса на этапе их передачи по сети (проводной или беспроводной).

••Перехват сеанса (session forging), когда злоумышленник использует идентификатор сеанса (возможно, полученный в ходе атаки с незаконным посредником), чтобы выдать себя за другого пользователя.

Эти атаки злоумышленник может провести, находясь со своим ноутбуком в кофейне, где имеется беспроводной доступ к Интернету, и перехватить сеансовые cookie. Впоследствии он может использовать перехваченный cookie, чтобы выдать себя за его настоящего пользователя.

•• Подделка cookie, когда злоумышленник подменяет данные в cookie, которые предположительно доступны только для чтения. В главе 14 подробно объясняется принцип работы cookie и отмечается, что и броузер, и злоумышленник могут спокойно изменить cookie без вашего ведома.

Есть множество историй о сайтах, которые хранили в cookie что-то типа IsLoggedIn=1 или даже LoggedInAsUser=jacob. Воспользоваться таким упущением просто до неприличия.

Поэтому нельзя доверять данным, хранящимся в cookie. Вам не дано знать, кто с ними поколдовал.

396

Глава 20. Безопасность

••Фиксация сеанса, когда злоумышленник обманом заставляет пользователя установить или переустановить идентификатор своего сеанса.

Например, PHP позволяет передавать идентификаторы сеанса внутри URL (http://example.com/?PHPSESSID=fa90197ca25f6ab40bb1374c510d 7a32). Если злоумышленник убедит пользователя щелкнуть на ссылке с фиксированным идентификатором сеанса, то пользователь превратится во владельца этого сеанса.

Фиксация сеанса применялась в различных вариантах фишинга, чтобы заставить пользователя ввести информацию в учетную запись, принадлежащую злоумышленнику. Затем он может зайти от имени этой учетной записи и прочитать введенные данные.

••Модификация сеанса, когда злоумышленник вставляет потенциально опасные данные в сеанс пользователя обычно посредством вебформы, при отправке которой устанавливаются сеансовые данные.

Канонический пример – сайт, который хранит в cookie простые настройки пользователя (скажем, цвет фона). Злоумышленник может обманом заставить пользователя щелкнуть на ссылке, якобы для того чтобы установить новый цвет. Только вместо «цвета» он получит сценарий, инициирующий XSS-атаку. Если значение цвета не экранируется, то вредоносный код может быть выполнен с правами данного пользователя.

Решение

Для защиты от таких атак есть ряд общих принципов:

•• Никогда не включайте в URL информацию о сеансе.

Подсистема­ сеансов в Django (глава 14) просто не допускает хранения сеансовых данных в URL.

•• Не храните в cookie сами данные. Там должен быть только идентификатор сеанса, по которому можно получить сеансовые данные, хранящиеся на сервере.

Сеансы, создаваемые Django (request.session) ведут себя именно так. Подсистема­ сеансов помещает в cookie только идентификатор, а данные сеанса хранятся в базе.

•• Не забывайте экранировать данные сеанса, если они отображаются

сиспользованием шаблона­. Прочитайте еще раз раздел о XSS-атаках

иобратите внимание, что все сказанное там применимо к любой информации, полученной от пользователя. Сеансовые данные относятся к той же категории.

•• По возможности мешайте злоумышленнику подделывать идентификаторы сеансов.

Обнаружить перехват идентификатора сеанса почти невозможно, тем не менее в Django имеется встроенная защита от атак на сеан-

Внедрение заголовков электронной почты

397

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

Отметим, что ни одно из вышеупомянутых средств защиты не способно предотвратить атаку с незаконным посредником, так как ее практически невозможно обнаружить. Если аутентифицированный пользователь может просматривать секретную информацию, то сайт в обязательном порядке должен работать по протоколу HTTPS. Кроме того, если сайт защищен SSL-шифрованием, то параметр SESSION_COOKIE_ SECURE должен быть равен True, тогда Django будет отправлять cookie только по защищенному соединению.

Внедрение заголовков электронной почты

У техники внедрения SQL есть менее известный родственник – внедрение заголовков электронной почты. Эта атака направлена на веб-формы, с помощью которых отправляется электронная почта. Данная разновидность атак может использоваться злоумышленником для рассылки спама через ваш почтовый сервер. Уязвимой является любая форма, при обработке которой на основе полученных данных конструируются заголовки электронной почты.

Рассмотрим типичную форму для ввода контактной информации, которая встречается на многих сайтах. Обычно при ее обработке посылается сообщение на фиксированный адрес, так что вроде бы никакой опасности спама нет. Однако в большинстве таких форм пользователю предоставляется возможность ввести еще и тему письма (а также адрес отправителя, тело и иногда еще какие-нибудь поля). На основе введенной темы конструируется заголовок «Subject» письма.

Если заголовок не экранируется, то злоумышленник может вписать в него что-то вроде “hello\ncc:spamvictim@example.com” (где “\n” – символ перевода строки). Тогда в письме появятся такие заголовки:

To: hardcoded@example.com Subject: hello

cc: spamvictim@example.com

Все, как при внедрении SQL, – если мы доверяем теме, которую ввел пользователь, то даем возможность сконструировать несанкционированные заголовки и рассылать спам с помощью нашей формы.

Решение

Для предотвращения такой атаки применяются те же принципы, что для предотвращения атаки внедрением SQL: всегда экранировать или проверять полученные от пользователя данные.

398

Глава 20. Безопасность

Встроенные в Django средства работы с электронной почтой (в пакете django.core.mail) просто не допускают появления символов новой строки в полях, предназначенных для построения заголовков (адрес отправителя, адрес получателя и тема). При попытке отправить с помощью django.core.mail.send_mail письмо, в теме которого встречается символ новой строки, Django возбудит исключение BadHeaderError.

Если вы не пользуетесь встроенными в Django функциями для отправки электронной почты, то сами делайте то же самое: либо возбуждайте исключение при обнаружении символов новой строки в заголовках, либо вырезайте эти символы. Можете изучить исходный код класса SafeMIMEText, если хотите узнать, как это делает Django.

Обход каталогов

Обход каталогов – еще один тип атак, основанный на приеме внедрения кода, когда злоумышленник ставит целью прочитать или записать файлы, находящиеся в каталогах файловой системы,­ к которым вебсервер, по идее, не должен иметь доступ.

Примером может служить представление, которое читает с диска файл, не проверив предварительно его имя:

def dump_file(request):

filename = request.GET[“filename”]

filename = os.path.join(BASE_PATH, filename) content = open(filename).read()

# ...

На первый взгляд, доступ разрешен только к файлам, которые находятся в поддереве с корнем BASE_PATH (ввиду вызова os.path.join). Но если злоумышленник передаст имя файла, в котором встречаются .. (две точки обозначают родительский каталог), то сможет получить доступ к файлам «выше» BASE_PATH. Очень скоро он выяснит, сколько раз нужно повторить точки, чтобы добраться до интересующего его файла, например ../../../../../etc/passwd.

Любой код, который читает файлы без надлежащего экранирования, – потенциальная жертва такой атаки. Уязвимы также представления, которые записывают файлы, только последствия могут быть еще печальнее.

Еще одно проявление той же ошибки – код, который динамически загружает модули, исходя из информации в запросе. Широко известен пример из фреймворка Ruby on Rails. До 2006 года в Rails использовались URL вида http://example.com/person/poke/1, которые позволяли напрямую загружать модули и вызывать методы. В результате искусно сконструированный URL позволял загрузить произвольный код, в том числе сценарий очистки базы данных!

Открытые сообщения об ошибках

399

Решение

Если вашей программе необходимо читать или записывать файлы, имена которых определяются полученными от пользователя данными, то необходимо тщательно проверять запрошенный путь и блокировать выход за пределы разрешенного участка файловой системы­.

Примечание

Без слов понятно, что код никогда не должен разрешать чтение произвольного файла на диске!

Пример такого экранирования можно найти во встроенном в Django представлении, предназначенном для обслуживания статического содержимого (в пакете django.views.static). Вот относящийся к делу код:

import os

import posixpath

# ...

path = posixpath.normpath(urllib.unquote(path)) newpath = ‘’

for part in path.split(‘/’): if not part:

# вырезать пустые компоненты пути continue

drive, part = os.path.splitdrive(part) head, part = os.path.split(part)

if part in (os.curdir, os.pardir):

# вырезать из пути ‘.’ и ‘..’ continue

newpath = os.path.join(newpath, part).replace(‘\\’, ‘/’)

Сам фреймворк Django не читает файлы (если только вы не пользуетесь функцией static.serve, но она, как видите, защищена), поэтому указанная уязвимость отсутствует в коде ядра.

Кроме того, механизм конфигурации URL гарантирует, что Django никогда не будет пытаться загрузить код, не указанный вами явно. Не существует способа создать URL, который заставил бы Django загрузить нечто, не упомянутое в конфигурации URL.

Открытые сообщения об ошибках

Возможность видеть в броузере трассировку и подробные сообщения об ошибках бесценна на этапе разработки. Чтобы упростить отладку, Django выводит красиво отформатированные и информативные сообщения.