Сообщить об ошибке.

Вопросы безопасности приложений на Flask

Веб-приложения постоянно сталкиваются со всевозможными проблемами безопасности. Flask решает некоторые вопросы безопасности, но не все, есть несколько моментов, о которой нужно позаботиться самостоятельно.

Содержание:


Безопасность сессии/сеанса приложения Flask.

Сессия/сеанс Flask позволяет запоминать информацию от одного запроса к другому и часто используются для доступа к важной информации (например, для доступа к панели администратора). Чтобы обезопасить данные фреймворк Flask подписывает сессионные файлы cookie при помощи секретного ключа SECRET_KEY. В результате чего пользователь может просматривать содержимое сессии/сеанса, но не может изменять его, если не знает секретный ключ. Один из способов создать сложный секретный ключ (его не нужно помнить), это воспользоваться следующей командой модуля os:

>>> import os
>>> os.urandom(30).hex()

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

Отнеситесь к этому серьезно. Не создавайте простых сессионных ключей, особенно из осмысленных слов и цифр, т.к. в индексе пакетов Python pypi есть модуль flask-unsign. Данный модуль представляет собой инструмент командной строки (CLI) для извлечения секретного ключа путем подбора по словарю (словарь прилагается) и дальнейшей подделки сеанса/сессии Flask путем изменения и внедрения своих cookie.

Не пренебрегайте этим пунктом! Очень часто веб-мастера горят на этом.

Важно! Помните, что файлы cookie сессии/сеанса Flask подписаны секретным ключом (защищены от подделки), НО НЕ ЗАШИФРОВАНЫ, благодаря этому можно локально декодировать данные сеанса. Следовательно НИКОГДА не записывайте в сессионные файлы cookie пароли и другую критически важную информацию из базы данных.

Опции безопасности заголовка Set-Cookie.

Эти параметры можно добавить в заголовок Set-Cookie для повышения их безопасности. Flask имеет параметры конфигурации, связанные с файлами сеансовых/сессионных cookie. Их так же можно установить и на другие файлы cookie.

  • Secure: ограничивает файлы cookie только HTTPS-трафиком.
  • HttpOnly: защищает содержимое файлов cookie от чтения с помощью JavaScript.
  • SameSite: ограничивает способ отправки файлов cookie с запросами с внешних сайтов. Может быть установлено значение 'Lax' (рекомендуется) или 'Strict'. Значение 'Lax' предотвращает отправку файлов cookie с запросами, подверженными CSRF, с внешних сайтов, таких как отправка формы. 'Strict' запрещает отправку файлов cookie со всеми внешними запросами, включая переход по обычным ссылкам.
app.config.update(
    SESSION_COOKIE_SECURE=True,
    SESSION_COOKIE_HTTPONLY=True,
    SESSION_COOKIE_SAMESITE='Lax',
)

response.set_cookie('username', 'flask', secure=True, httponly=True, samesite='Lax')

Если указать параметры Expires или Max-Age, то cookie будет удален по истечении заданного времени или текущего времени плюс возраст, соответственно. Если ни одна из опций не установлена, то cookie будет удален при закрытии браузера.

# срок действия файлов `cookie` истекает через 10 минут
response.set_cookie('snakes', '3', max_age=600)

Если установлен session.permanent, то параметр PERMANENT_SESSION_LIFETIME используется для установки срока действия cookie сессии/сеанса. По умолчанию, реализация файлов cookie в Flask проверяет, что криптографическая подпись не старше этого значения. Понижение этого значения может помочь уменьшить атаки повторного воспроизведения, когда перехваченные файлы cookie могут быть отправлены позже.

app.config.update(
    PERMANENT_SESSION_LIFETIME=600
)

@app.route('/login', methods=['POST'])
def login():
    ...
    session.clear()
    session['user_id'] = user.id
    session.permanent = True
    ...

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

Межсайтовый скриптинг - это концепция внедрения произвольного HTML, а вместе с ним и JavaScript в контекст веб-сайта. Чтобы не допустить XSS атак, разработчики должны правильно экранировать текст, чтобы он не мог включать произвольные теги HTML.

Фреимворк Flask настраивает шаблонизатор Jinja2 на автоматическое экранирование всех значений, если явно не указано иное. Это должно исключить все проблемы XSS, но есть и другие места, где необходимо быть осторожным:

  1. Создание и вывод HTML на сайт, минуя шаблонизатор Jinja2.
  2. Вывод HTML данных, которые были отправлены пользователем.
  3. Вывод контента из загруженных HTML-файлов, никогда не делайте этого, используйте заголовок Content-Disposition: attachment, чтобы предотвратить эту проблему.
  4. Вывод текстовых файлов, которые были загружены. Некоторые браузеры используют угадывание типа контента на основе первых нескольких байтов, так вот пользователи могут обмануть браузер для выполнения HTML.

Еще одна важная вещь - это HTML-атрибуты без кавычек. Хотя Jinja2 защищает от основных проблем с XSS, экранируя HTML, но есть две разновидность атаки, с которой шаблонизатор не справится. Одна из них - это XSS путем внедрения атрибутов. Чтобы противостоять этому вектору атаки, обязательно и всегда заключайте HTML-атрибуты в двойные или одинарные кавычки, особенно при использовании в них выражений Jinja:

<input value="{{ value }}">

Вторая проблема с XSS, от которых автоэкранирование Jinja не защищает. Атрибут href HTML-тега <a> может содержать код javascript: URI, который браузер выполнит при нажатии, если он не защищен должным образом.

<a href="{{ value }}">click here</a>
<a href="javascript:alert('unsafe');">click here</a>

Чтобы этого не произошло, нужно установить заголовок ответа Content Security Policy (CSP).

Подделка межсайтовых запросов (CSRF).

Еще большая проблема - это CSRF. Это очень сложная тема. Короче говоря, если информация об аутентификации хранится в файлах cookie, то имеется неявное управление состоянием “входа в систему”. Это состояние контролируется файлом cookie, и этот файл cookie отправляется с каждым запросом на страницу. К сожалению, это включает в себя запросы, инициированные сторонними сайтами. Если не принимать данный факт во внимание, то некоторые нехорошие люди могут обмануть пользователей сайта с помощью социальной инженерии, чтобы сделать глупые вещи без их ведома.

Допустим, есть конкретный URL-адрес, по которому, при отправке POST-запроса будет удален профиль пользователя (например, http://example.com/user/delete). Если злоумышленник создаст некую страницу, которая отправляет POST-запрос на страницу http://example.com/user/delete с помощью некоторого JavaScript, то теперь ему (злоумышленнику) нужно обманным путем заставить пользователя загрузить поддельную страницу, и профиль пользователя будет удален (т.к. информация об аутентификации хранится в файлах cookie браузера пользователя).

Как можно этого предотвратить? Для каждого запроса, который изменяет контент в базе данных или на сервере, нужно использовать одноразовый токен и сохранить его в файле cookie, а также передавать его с данными формы. После повторного получения данных, на сервере, необходимо сравнить два токена на равенство.

Почему Flask этого не делает? Идеальное место для этого - это проверка переданной с сайта формы, которой во Flask НЕТ. Но есть расширение Flask-WTF, которое прекрасно с этим справляется и позволяет легко создавать HTML-формы.

HTTP-заголовки безопасности.

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

HTTP Strict Transport Security (HSTS)

Указывает браузеру преобразовать все HTTP-запросы в HTTPS, предотвращая атаки типа "злоумышленник в середине" (MITM).

response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'

Смотрите дополнительно: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security

Content Security Policy (CSP)

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

Пример очень строгой политики:

response.headers['Content-Security-Policy'] = "default-src 'self'"

Дополнительно смотрите:

X-Content-Type-Options.

Заставляет браузер учитывать тип содержимого ответа вместо его обнаружения, что может быть использовано для создания атаки межсайтового скриптинга (XSS).

response.headers['X-Content-Type-Options'] = 'nosniff'

Смотрите дополнительно: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options

X-Frame-Options.

Запрещает внешним сайтам встраивать сайт в iframe. Это предотвращает класс атак, при которых клики во внешнем фрейме могут быть незримо преобразованы в клики по элементам страницы вашего сайта. Атака известна как "кликджекинг".

response.headers['X-Frame-Options'] = 'SAMEORIGIN'

Смотрите дополнительно: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options

X-XSS-Protection.

Браузер IE и Safari попытается предотвратить отраженные XSS-атаки, не загружая страницу, если запрос содержит что-то похожее на JavaScript, а ответ содержит те же данные. Обратите внимание, что Chrome удалил этот заголовок, а Firefox никогда его не поддерживал.

response.headers['X-XSS-Protection'] = '1; mode=block'

Злоупотребление буфером обмена с веб-сайтов.

Многие веб-сайты используют JavaScript или CSS для скрытой вставки или замены текста в буфер обмена пользователя во время копирования информацию со страницы. НИКОГДА, сразу, не вставляйте скопированный код со стороннего сайта в терминал или интерпретатор Python, каким бы безобидным он не казался. Скопированный код могут подменить (опасно, не вставлять!!!) например на rm -rf ~/ или import shutil; shutil.rmtree('~/')

А может быть по другому: скрытые символы, такие как \b и ^H, могут привести к тому, что текст в HTML будет отображаться иначе, чем он интерпретируется при вставке в терминал. Например, import y\bose\bm\bi\bt\be\b отображается в HTML как import yosemite, но при вставке в терминал применятся применяются \b и он становится import os.

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

comment = comment.replace("\b", "")