TL;DR
Статья является обзором на книгу "Безопасность веб-приложений" Эндрю Хоффмана, варианты векторов атак и защиты из книги разобраны на "реальных" примерах ниже.
Вася, неплохой веб-разработчик, решил создать свой прекрасный интернет-магазин, который приносил бы ему пассивный доход. Вася набил руку за несколько лет работы веб-разработчиком и считает, что сделать это будет раз плюнуть. Раскруткой и рекламой будет заниматься его друг, поэтому они не будут разобраны в статье этими аспектами Василий не интересуется.
Сторонние зависимости
Как и все разработчики, Вася использует библиотеки, чтобы написать свой сайт как можно быстрее и удобнее. Библиотеки он берёт как общепризнанные и популярные, так и увиденные им где-то в статьях на Медиуме/Хабре/<ЛюбойДругойРесурс>. При запуске прототипа магазина у себя на локальной машине Вася видит загрузку CPU максимум на всех ядрах, при заходе на любую страницу через браузер. Открыв вкладку network Вася видит много запросов на какую-то крипто биржу, и понимает, что в его приложении где-то лежит встроенный майнер.
Проблема с вредоносным кодом в зависимостях не нова. Может произойти всё что угодно - от майнинга до слития данных злоумышленнику. Пакеты с вредоносным кодом могут быть как в популярных библиотеках, так и в мимикрирующих под них - пример 1, пример 2, пример 3. Помимо этого в самих библиотеках могут быть CVE-уязвимости.
Как себя обезопасить?
Использовать сканер уязвимостей. Например, у Python есть safety, у npm - встроенный npm audit.
Использовать принцип минимальных привилегий. Веб-приложение должно иметь доступ только туда, куда ему необходимо, и никуда больше. В эпоху
контейнеровдокера, где по умолчанию всё запускается из-под рута, неплохо бы запускать приложение от имени определённого пользователя с ограниченными правами.Использовать зависимости только определённых версий. Никогда не знаешь в какой версии появляется та или иная уязвимость, поэтому крайне рекомендую использовать статичную версию, ещё лучше - с хэшем, т.к. версию можно подменить. а хэш - нет. Но это не значит, что версия должна быть старая. Она просто должна быть зафиксирована и проверена, хотя бы сканером уязвимостей, перед её использованием. В Python можно использовать команду pip freeze или другие утилиты, такие как pip-compile-multi, poetry, pipenv и другие
Настроить CSP политику. На бэкенде можно задать список разрешённых хостов, откуда фронтенд может скачивать статику. Это может быть полезно, чтобы исключить вариант, когда в ссылке есть ссылка на ссылку на вредоносный код. У Django, как вариант, есть django-csp.
Вася, прочитав пару статей в интернете, начал использовать определённые версии пакетов и теперь периодически проверяет их сканером уязвимостей. Также он ставит зависимости внимательно, чтобы случайно не установить вредоносный пакет, отличающийся на один символ от оригинала. Даже почитал про CSP и добавил в разрешённые хосты только необходимые.
XSS
Спустя время наш герой выпускает в интернет свою первую версию приложения, но тут же натыкается на проблему. Он сделал форму обратной связи, где пользователь может оставить отзыв, который Вася может открыть и прочитать. Но вот незадача - при открытии страницы Василий видит следующее:
Как такое произошло? Ведь Вася думал, что всё предусмотрел. Однако мало обеспечить безопасность зависимостей - нужно сделать безопасным свой код. Вася наткнулся на типичную XSS атаку - он не проверял пользовательский ввод на наличие html и js кода, а после вставлял к себе на страницу как есть.
Как себя обезопасить?
Так как типы XSS атак могут быть разными, то разная может быть и защита. Вася столкнулся с самым простым, но опасным типом - хранимым XSS. Варианты защиты самые разные:
Периодически сканировать базу - поиск html тегов, их удаление через регулярку
При записи в базу проверять пользовательский ввод и выводить сообщение об ошибке или молча запрещать сохранять потенциальный XSS в базу
Если html теги всё же нужны - сделать список разрешённых тегов
Добавление данных на страницу исключительно как строку (innerText вместо innerHTML)
Также XSS может быть отражённым. Например:
Злоумышленник внедряет в гиперссылку вредоносный скрипт, позволяющий просматривать cookies пользовательской сессии, и отправляет жертве по электронной почте или другим средством коммуникации.
При переходе по ссылке пользователь становится захваченным.
Скрипт выполняется в браузере пользователя.
Браузер отправляет cookies злоумышленнику, обеспечивая доступ к личным данным пользователя.
Как себя обезопасить?
Проверка uri параметров перед их выполнением. Например, какой-то из параметров в процессе выполнения может вставляться через element.innerHTML на страницу.
Обязательный urlencode ссылок перед их выполнением. К примеру, в JavaScript можно вставить ссылку в a.href атрибут, который выполняет urlencode.
XSS на базе DOM. Это тот же хранимый XSS, который не обязательно может храниться в базе. Например, вредоносный код в document.cookie. Или же внутри SVG/XML кода.
Как себя обезопасить?
По возможности избегать element.innerHTML, element.outerHTML, Blob, SVG, document.write, document.writeln, DOMParser.parseFromString, document.implementation.
По возможности запретить пользователю делать свою таблицу стилей. Или, по крайней мере, жёстко ограничить варианты.
Потратив время на изучение XSS Вася обезопасил себя как только мог, проверил возможные места, где HTML мог вставляться в DOM, не пользуется innerHTML и подобными и всегда делает urlencode ссылок.
XXE
Казалось бы, он защитился везде как только можно, однако в какой-то момент он переходит на свою страницу с обратной связью и снова видит:
Что же теперь могло произойти? Дело в том, что при открытии страницы с обратной связью для Васи также подгружается аватар пользователя. Этот аватар может быть SVG файлом. А внутри SVG файла и находится этот злополучный алерт! Вася столкнулся с типичной XXE атакой, ведь SVG файл это тот же XML! Васе повезло уже в который раз, что там такой безобидный вредоносный код. А ведь всё могло закончиться куда хуже.
Как себя обезопасить?
XXE атака может быть как прямой (как в данном случае), так и непрямой (когда XML генерируется на основании входных данных запроса на сервер, в которых может быть вредоносный код). Вариантов защититься, на самом деле, немного:
По возможности отключить внешние сущности в XML анализаторе. XML парсер можно настроить таким образом, чтобы исключить возможный вредоносный код или вредоносные запросы. В разных анализаторах разные принципы. Например, Bandit в Python умеет это проверять.
Заменить XML на JSON (YAML, BSON, EDN и др), если возможно
Вася, вытирая пот с лица, запретил в качестве иконок пользователей формат SVG и полностью отказался от формата XML. Ведь проще, если это возможно, полностью убрать вектор атаки, чем его тюнить, с надеждой на то, что полностью исключил угрозу. SVG теперь Вася грузит только для эмблемы своего интернет-магазина из доверенного CDN.
CSRF
У Васи дела начинают идти хорошо - всё больше пользователей заходят на его сайт и даже делают покупки. В какой-то момент, в форме обратной связи, Васе приходит отзыв пользователя Пети, что у него со счёта списываются деньги через наш сайт. И правда, судя по логам, Вася видит, что Петя делал покупки, однако на странный адрес доставки и нетипичные для этого пользователя товары. В чём же проблема? Спустя некоторое время Вася выпытал у Пети, что это произошло после того, как пользователь хотел получить выплаты от государства, и переходил по сомнительным ссылкам, которые предлагали оплатить комиссию перед тем, как он получит свои выплаты. Однако Петя умный и не стал платить комиссию в столь очевидном лохотроне. Но заходил то он на чужие ссылки, а списалось с Васиного интернет-магазина. Что делать?
Вася столкнулся с первой серьёзной проблемой, в которой ему пришлось возместить деньги пользователю, т.к. Вася дорожит репутацией. А проблема заключается в том, что у Васи нет CSRF защиты для эндпоинтов с изменением состояния (CRUD запросов). Злоумышленник встроил скрипт на свой сайт, чтобы тайно делался запрос от пользователя на оплату товаров в магазине Васи. Вася польщён, что для его интернет-магазина есть целый вредоносный скрипт, однако решает защититься.
Как себя обезопасить?
Для запросов с изменением состояния (
POST/PUT/DELETE
) использовать CSRF токен
Запросы
GET/HEAD/OPTION
должны быть без сохранения состоянияCSRF защита должна быть на уровне приложения в целом, а не на уровне отдельных вьюх. В Django, к примеру, есть встроенная мидлваря
CsrfViewMiddleware
.
Правда, сейчас этот вектор атаки уже не так актуален
Существуют разные источники, в которых говорится, что 99% от этой проблемы ушло с помощью SameSite
атрибута в куках, которые поддерживаются уже достаточно долго во всех современных браузерах. Однако это не спасёт, если пользователь заходит со старого браузера или чтобы защититься от атаки с сабдомена (если SameSite=Lax
). Подробнее по ссылке выше.
Вася подключил CSRF мидлварю в свой фреймворк и попробовал пройти по ссылкам, которые любезно предоставил Петя, увидел пару странных запросов в логах, но с карты Васи ничего не списалось. Вася перепроверил, теперь его GET/HEAD/OPTION
запросы точно не меняют состояние.
CORS
Василий исправил очередную уязвимость и неимоверно горд собой! Правда, теперь он скорее ждёт - а что же будет дальше и решил изучить наперёд ещё виды защиты своего сайта. И его взгляд упал на CORS. CORS позволяет выполнять запросы с другого домена, который разрешён настройками CORS. По умолчанию CORS запрещён. Что же ему делать?
Внимательно прочитать все заголовки CORS и заняться их тонкой настройкой
Изучить и использовать уже существующие библиотеки для реализации CORS механизма. В Python у Django, к примеру, есть замечательная библиотека django-cors-headers
Также внимательный читатель увидит, что CSRF уже частично спасает от запросов со сторонних сайтов. Однако CORS лишь дополняет CSRF, но не может его заменить, т.к. CORS позволяет выполниться запросу, но не позволяет получить результат выполнения. Это спасает от запросов без изменения состояния, но, например, если во время запроса отправляется письмо, то с настроенным CORS мы просто не увидим вывод эндпоинта, но письмо отправится в любом случае.
Code injection
Магазин у Васи работает уже не одну неделю и начинает приносить ему прибыль. Он вносит всё больше фич, даже решил сделать очень кастомизированный интерфейс для просмотра заказов. Спустя пару дней ему приходят резко сотни отзывов, что пользователи не могут зайти в аккаунт. Вася с ужасом обнаруживает, что в его базе пропали все пользователи! В срочном порядке он накатывает бэкап базы за вчера и быстро решает проблемы пользователей по поводу пропавших заказов. Но также, в срочном порядке, закопавшись в код, ищет причину проблемы. И по логам находит странный запрос от одного из пользователей, после которого повалили обращения. И видит там запрос, примерно как на картинке выше. Вася начинает искать решение и открывает для себя SQL Injection.
Как себя обезопасить?
SQL Injection лишь один из нескольких возможных Code Injection. Другие связаны с запуском произвольного запроса в командной строке через параметры запроса, или, к примеру, перезаписи существующего файла статики из-за конфликта имён и плохо спроектированного API.
Защита от SQL инъекций
Использовать orm или query builder
Подготовленные операторы (при использовании raw sql, например PREPARE в PostgreSQL)
Экранирование SQL с помощью средств библиотеки
Защита от других видов внедрения
Поиск потенциальных мест внедрения (командная строка, интерпретатор, библиотеки сжатия, диспетчеры задач, сценарии удалённого резервного копирования, журналы)
Принцип минимальных привелегий (мы уже встречались с ним, когда говорили о сторонних зависимостях)
Белый список команд (проще контролировать определённые команды, нежели запрещать все остальные).
Теперь Вася осторожнее и вдумчиво пилит новые фичи, предоставляя пользователям только то, что ему нужно, прикрыв старые SQL Injection уязвимости и с помощью ORM и через возможности экранирования в psycopg2 библиотеке там, где ORM использовать не получается.
DoS
Интернет-магазин Василия имеет популярность, его DAU насчитывает уже более трёхсот, а WAU - более тысячи пользователей. Он собирается в скором времени увольняться с основной работы, т.к. его интернет-магазин начал приносить уже весомую прибыль. Но в один из дней Вася замечает, что его интернет-магазин перестал работать и отдаёт 521 Web Server Is Down
. Василий судорожно начинает, в очередной раз, читать логи и видит огромную активность у анонимов на главной странице, из-за которых заняты все CPU ресурсы. Он раньше только слышал, но понимает, что это DDoS.
Вася столкнулся лишь с одной разновидностью DoS атаки. Существует ещё парочка:
ReDoS (regular-expression-based DoS) - атака на эндпоинты, использующие регекс, из-за вызова которых в большом количестве может замедлиться работа приложения. Например, регекс проверки пароля или составленные пользователем регексы для какой-либо задачи (evil regex, malicious regex) или слишком сложные, составленные разработчиком. Правильно подобранная строка может парситься на порядок, а то и несколько порядков дольше, чем остальные.
логические DoS уязвимости - злоумышленник шлёт много запросов, требующих много вычислительных ресурсов.
Как защититься:
Фиксация всех запросов вместе с количеством времени, которое было потрачено на их выполнение
Для ReDoS атак:
Искать регулярное выражение, которое приводит к поиску с возвратом, использовать инструменты OSS для поиска, синтаксический анализатор. Я для себя открыл regexploit.
По возможности запретить пользовательский ввод регулярных выражений
Для логических DoS атак:
Определить области кодовой базы, в которых используются критические системные ресурсы
Троттлинг/задержки/локи
для DDoS атак:
Вложиться в службу управления брандмауэром
Фильтрация чёрных дыр
Вася, тем временем, ставит троттлинг для анонимов на главную страницу, и со временем атака утихает. Для защиты от более умных хакеров уже написали статью за меня пару дней назад.
Безопасное программирование
За прошедший год работы Вася находил и устранял уязвимости в безопасности своего интернет-магазина после внесения фич, ведь он уже знал, к чему могут привести эти проблемы, и что чем раньше их решить, тем больше нервов у него останется. Василий уволился и занимался лишь своим интернет-магазином последние полгода. Он выработал свод антипаттернов безопасного программирования опытным путём и решил выложить их для начинающих разработчиков:
чёрные списки -> белые списки
шаблонный код -> дополнительные усилия для безопасности (донастройка конфигураций, устранение параметров по умолчанию, по которым можно определить софт, например, использовать кастомные стили при стандартных ошибках в фреймворке или сделать свои страницы об ошибках)
разрешения на взаимодействие с системой или внешними компонентами вместо "рута" (rwx права на определённые папки, доступы в базу в зависимости от роли)
разделение клиента и сервера (не должны брать ответственность друг друга)
Также Вася задумался о термине "Безопасное программирование" и систематизировал свои знания:
Анализ требований к ПО
какими данными обменивается клиент и сервер
как эти данные интерпретируются сервером
оценка баз данных, журналов регистрации, загруженных файлов, библиотек и всего, что вызывают конечные API точки
оставшаяся часть функциональности, предоставляемая клиенту
проверка на доступы API точек
оставшийся код
Аутентификация и авторизация
использовать SSL/TLS, чтоб обеспечить защиту от MITM атак.
защита учётных данных (как и где эти данные будут храниться. Доступы к ним. и т.п.),
хэширование паролей,
2FA
Итог
В самом начале Вася недооценивал необходимость базовой защиты своего интернет-магазина, т.к. не считал, что это полезная трата времени. Однако время показало, что он заблуждался, и это чуть ли не самое важное после фич. В последние дни, во время хакерских взломов, защита веб-приложений актуальна как никогда. Я понимаю, что описал лишь самые базовые вещи, и что защита - это не сделал и забыл, а процесс, актуализировать который нужно постоянно, чтобы не обнаружить потерю пользовательских данных, репутации и денег. Успехов в создании своего "интернет-магазина", друзья!