Безопасность веб-приложений: как защищаются данные, пользователи и сервер
Безопасность веб-приложения часто представляют как список отдельных атак:
SQL Injection - внедрение SQL-кода через входные данные
XSS - выполнение чужого кода в браузере
CSRF - выполнение действия от имени авторизованного пользователя
Brute force - массовый перебор вариантов доступа
Но атака - только один элемент системы.
Чтобы понимать безопасность, сначала нужно определить:
- что мы защищаем;
- от каких действий защищаем;
- где система может быть нарушена;
- какие последствия допустимы, а какие нет;
- какие механизмы должны предотвращать нарушение.
Без этой основы отдельные правила вроде «экранируй HTML» или «используй HTTPS» превращаются в набор несвязанных советов.
Что такое информационная безопасность
Информационная безопасность - защита информации и систем от недопустимого доступа, изменения, раскрытия, уничтожения и нарушения работы.
Обычно выделяют три базовых свойства:
Конфиденциальность - данные доступны только тем, кому они разрешены
Целостность - данные не изменены недопустимым образом
Доступность - система и данные доступны тогда, когда они нужны
Например:
Чужой пользователь прочитал адрес клиента - нарушена конфиденциальность
Злоумышленник изменил сумму заказа - нарушена целостность
Сервис перестал принимать запросы - нарушена доступность
Безопасность не сводится только к защите секретных данных. Иногда основная цель атаки - изменить информацию или сделать сервис недоступным.
Что защищает веб-приложение
Сначала нужно определить активы.
Актив - объект, потеря, раскрытие или изменение которого причиняет системе вред.
Активами могут быть:
- учётные записи;
- персональные данные;
- заказы;
- платежная информация;
- документы;
- API-ключи;
- исходный код;
- база данных;
- вычислительные ресурсы;
- административный интерфейс;
- доступность системы.
Актив - то, что имеет ценность и должно быть защищено
Не все активы одинаково критичны.
Публичное изображение товара - низкая конфиденциальность
Пароль пользователя - критичные данные доступа
Ключ администратора - критичный секрет
Уровень защиты зависит от ценности актива и возможных последствий нарушения.
Угроза, уязвимость, атака и риск
Эти понятия связаны, но не являются синонимами.
Угроза - потенциальная причина нарушения безопасности
Уязвимость - слабое место системы
Атака - действие, использующее слабость системы
Риск - вероятность проблемы с учётом её последствий
Рассмотрим ситуацию, когда приложение строит SQL-запрос из строки пользователя:
query = f"SELECT * FROM users WHERE email = '{email}'"
Здесь:
Актив - данные пользователей
Угроза - попытка получить чужие данные
Уязвимость - ввод вставляется прямо в SQL
Атака - SQL Injection
Последствие - чтение или изменение данных
Сама уязвимость ещё не является атакой. Она создаёт возможность для неё.
Поверхность атаки
Поверхность атаки - все точки системы, через которые на неё можно воздействовать.
Для веб-приложения это могут быть:
- публичные страницы;
- формы;
- API;
- webhook endpoint;
- административная панель;
- загрузка файлов;
- авторизация;
- восстановление пароля;
- внешние интеграции;
- база данных;
- файловое хранилище.
Новый endpoint - новая точка взаимодействия
Новая интеграция - новая граница между системами
Новый тип файла - новый вид внешних данных
Уменьшение поверхности атаки означает, что ненужные точки доступа не должны существовать.
Граница доверия
Граница доверия появляется там, где данные переходят между областями с разным уровнем доверия.
Браузер пользователя - сервер
Сервер - база данных
Backend - внешний API
Webhook-отправитель - webhook-получатель
Администратор - административный интерфейс
Данные не становятся безопасными только потому, что пришли из другого компонента системы.
Например, frontend может проверить:
Цена должна быть больше 0
Но сервер не должен доверять этой проверке. HTTP-запрос можно сформировать без пользовательского интерфейса.
Поэтому правила закрепляются на соответствующих уровнях:
Frontend - удобство и ранняя проверка
Backend - допустимость операции и права
База данных - структурная целостность данных
Почему нельзя доверять входным данным
Внешними данными являются не только значения из HTML-формы.
Источником может быть:
- пользователь;
- браузер;
- мобильное приложение;
- внешний API;
- webhook;
- файл;
- URL;
- cookie;
- HTTP-заголовок.
Проверка должна отвечать на несколько вопросов:
Тип - соответствует ли значение ожидаемому типу
Формат - соответствует ли структура правилам
Диапазон - допустимо ли значение
Размер - не превышает ли данные ограничение
Права - может ли отправитель задавать это значение
Контекст - безопасно ли использовать данные дальше
Например:
quantity = 5 - корректно
quantity = -100 - тип корректный, значение недопустимо
quantity = "abc" - неверный тип
Валидация определяется не только синтаксисом, но и правилами предметной области.
Аутентификация и авторизация
Это разные этапы.
Аутентификация - определить, кто выполняет запрос
Авторизация - определить, разрешено ли ему действие
Пользователь может успешно войти в систему, но не иметь права открыть чужой заказ.
Пользователь известен - аутентификация пройдена
Пользователь может читать order_id=42 - требуется авторизация
Недостаточно проверить только факт входа:
if user.is_authenticated:
return order
Нужно проверить право на конкретный ресурс и действие.
Пользователь вошёл - недостаточно
Пользователь имеет право на ресурс - необходимая проверка
Принцип минимальных привилегий
Каждому пользователю и компоненту дают только те права, которые нужны для его задачи.
Менеджер - работа с заказами
Бухгалтер - финансовые данные
Контент-менеджер - статьи
Администратор - системные операции
Если сервису нужно только читать таблицу, ему не требуется право удаления.
Необходимая операция - необходимое право
Лишнее право - дополнительный риск
Этот принцип относится к:
- пользователям;
- приложениям;
- API-ключам;
- сервисным аккаунтам;
- базам данных;
- файловым хранилищам.
Пароли
Пароль нельзя хранить в открытом виде.
Также его обычно не нужно хранить так, чтобы система могла восстановить исходное значение.
Для хранения используют алгоритмы password hashing.
Пароль - password hashing - сохранённое значение
При входе система проверяет новый ввод относительно сохранённого результата.
К паролю добавляют случайную соль.
Salt - случайное значение, используемое при вычислении хеша пароля
Для паролей применяют алгоритмы, специально рассчитанные на дорогое вычисление. Обычный быстрый SHA-256 сам по себе не является подходящей схемой хранения паролей.
Сессии и токены
После входа серверу нужно узнавать пользователя в следующих запросах.
Один из способов - сессия.
Вход - сервер создаёт сессию - браузер получает идентификатор - следующие запросы содержат идентификатор
Другой способ - токен.
Токен - значение, подтверждающее определённый контекст доступа
Наличие токена не отменяет авторизацию.
Токен определил пользователя - сервер проверил права - операция разрешена или отклонена
Cookie и их ограничения
Cookie автоматически отправляются браузером на подходящий адрес.
Для чувствительных cookie используются дополнительные параметры:
HttpOnly - ограничивает доступ JavaScript к cookie
Secure - cookie передаётся по защищённому соединению
SameSite - ограничивает межсайтовую отправку cookie
Эти параметры решают разные задачи и не заменяют друг друга.
SQL Injection
SQL Injection появляется, когда внешние данные превращаются в часть SQL-кода.
Опасная схема:
SQL-код + строка пользователя - одна команда
Защита строится на разделении структуры запроса и данных.
SQL-команда - структура
Параметр - значение
Параметризованный запрос не должен интерпретировать значение как SQL-синтаксис.
ORM обычно делает это при стандартном использовании, но ручная сборка raw SQL строками может вернуть уязвимость.
XSS
XSS появляется, когда пользовательские данные попадают в HTML как исполняемый код.
Например, пользователь сохранил:
<script>...</script>
Если приложение вставит строку как доверенный HTML, браузер может выполнить код.
Основной принцип:
Пользовательские данные - данные
HTML приложения - код
Экранирование не позволяет специальным символам изменить структуру HTML.
Особое внимание требуется там, где приложение намеренно разрешает пользовательский HTML.
CSRF
Браузер может автоматически отправлять cookie вместе с запросом.
Из-за этого другая страница способна попытаться заставить браузер авторизованного пользователя выполнить действие на целевом сервисе.
CSRF-защита проверяет допустимость изменяющего запроса.
Cookie - подтверждает сессию
CSRF-механизм - помогает проверить допустимость происхождения действия
CSRF не заменяет авторизацию.
CORS и Same-Origin Policy
Браузер ограничивает взаимодействие JavaScript между разными origin.
Origin определяется сочетанием:
Схема + домен + порт
CORS позволяет серверу сообщить браузеру, каким внешним origin разрешено читать определённые ответы.
Важно разделять:
CORS - политика браузерного доступа
Авторизация - проверка права пользователя или клиента
CORS не является системой прав API.
HTTPS
HTTPS использует TLS для защиты передачи данных между сторонами.
Основные свойства:
Шифрование - скрывает содержимое трафика
Целостность - позволяет обнаружить изменение данных
Аутентификация сервера - сертификат помогает проверить сторону соединения
HTTPS защищает канал, но не исправляет ошибки логики приложения.
HTTPS + неправильная авторизация - данные всё равно могут быть раскрыты
Безопасность API
API принимает запросы непосредственно от программ, поэтому нельзя полагаться на ограничения пользовательского интерфейса.
Для endpoint нужно определить:
Кто может вызвать
Какие данные разрешены
Как определяется пользователь или сервис
Как проверяются права
Как ограничивается частота
Какие поля возвращаются
Что записывается в аудит
Сервер не должен возвращать чувствительные поля только потому, что они существуют в объекте модели.
Rate limiting
Даже корректный запрос может стать проблемой при слишком высокой частоте.
Rate limiting ограничивает количество операций за период.
Он особенно полезен для:
- входа;
- восстановления пароля;
- отправки сообщений;
- тяжёлых API-методов;
- публичных endpoint;
- внешних интеграций.
Частота допустима - запрос выполняется
Лимит превышен - запрос ограничивается
Webhook и подпись запроса
Webhook endpoint доступен внешнему сервису. Получателю нужно проверить, действительно ли запрос сформирован ожидаемой стороной.
Один из механизмов - подпись.
Raw body + секрет - HMAC - ожидаемая подпись
Заголовок webhook - полученная подпись
Совпадение - криптографическая проверка пройдена
Часто используется HMAC-SHA256.
SHA-256 - криптографическая хеш-функция
HMAC - механизм вычисления подписи с секретным ключом
Подпись обычно вычисляют по исходным байтам тела запроса, поэтому изменившееся представление JSON может дать другой результат.
Replay attack
Корректно подписанный запрос можно попытаться отправить повторно.
Если операция означает:
Начислить деньги
Создать платёж
Выдать доступ
повторное выполнение недопустимо.
Используются несколько механизмов:
Timestamp - запрос не должен быть слишком старым
Event ID или nonce - событие не должно быть обработано повторно
Idempotency - повторная доставка не создаёт второе бизнес-действие
Загрузка файлов
Файл является внешними данными.
Нельзя доверять только расширению:
photo.jpg - имя не гарантирует содержимое
При загрузке учитывают:
- формат;
- MIME type;
- размер;
- имя;
- место хранения;
- права доступа;
- дальнейшую обработку.
Пользовательский файл не должен автоматически становиться исполняемой частью приложения.
Секреты
Секрет - значение, владение которым даёт доступ к защищённой операции или системе.
Пароль базы данных - secret
API key - secret
Webhook secret - secret
Private key - secret
Access token - secret
Секреты не следует хранить прямо в исходном коде.
Код - логика
Конфигурация - параметры окружения
Secret - защищённое значение
Особенно опасна публикация секрета в истории Git.
Логирование и аудит
Система должна позволять понять:
Что произошло
Когда произошло
Кто выполнил действие
К какому объекту обращались
Каков результат
Технический лог и audit log решают разные задачи.
Технический лог - как работала программа
Audit log - кто совершил значимое действие
Для аудита могут сохраняться:
user_id - пользователь
action - действие
object_id - объект
timestamp - время
result - результат
request_id - идентификатор запроса
В логи нельзя без необходимости помещать:
- пароли;
- полные access token;
- приватные ключи;
- секреты;
- чувствительные данные.
Request ID
Один пользовательский запрос может пройти через несколько компонентов.
HTTP-запрос - request_id
Backend - тот же request_id
Внешний сервис - request_id
Логи - request_id
Это упрощает анализ ошибок и подозрительных действий.
Многоуровневая защита
Нельзя рассчитывать на один механизм.
Например, изменение заказа может защищаться несколькими слоями:
HTTPS - аутентификация - авторизация - валидация - ограничения базы - транзакция - аудит
Если один слой содержит ошибку, другой может уменьшить последствия.
Defense in depth - несколько независимых уровней защиты системы
Безопасность на разных уровнях приложения
Одна операция проходит через несколько компонентов.
Пользователь - интерфейс - HTTP - backend - база данных
Интерфейс
Интерфейс:
- помогает вводить корректные данные;
- скрывает недоступные действия;
- показывает ошибки.
Но интерфейс не является границей безопасности.
Backend
Backend:
- аутентифицирует;
- авторизует;
- валидирует;
- выполняет бизнес-правила;
- ограничивает действия.
База данных
База может закреплять:
NOT NULL - обязательность
UNIQUE - уникальность
CHECK - допустимость значения
FOREIGN KEY - ссылочную целостность
Transaction - атомарность связанных изменений
Ограничения базы защищают данные независимо от конкретного интерфейса приложения.
Как анализировать безопасность функции
Пусть существует операция:
DELETE /orders/42
Для неё можно последовательно спросить:
Актив - что защищаем
Пользователь - кто вызывает
Аутентификация - как он определён
Авторизация - может ли удалять этот заказ
Ввод - корректен ли order_id
Бизнес-правило - можно ли удалять заказ в текущем состоянии
Данные - не нарушатся ли связи
Аудит - будет ли действие зафиксировано
Так безопасность становится частью проектирования операции, а не дополнительной проверкой после реализации.
Что безопасность не гарантирует автоматически
Не существует одного фреймворка или библиотеки, после подключения которой приложение становится безопасным.
Остаются:
- ошибки бизнес-логики;
- неправильные права;
- утечки секретов;
- неверная конфигурация;
- уязвимые зависимости;
- человеческие ошибки;
- ошибки интеграций.
Главная задача - понимать границы системы и каждое решение о доверии.
Итог
Безопасность веб-приложения начинается не с названий атак.
Основная цепочка:
Актив - угроза - уязвимость - атака - последствие - риск - защитный механизм
На уровне приложения защита строится из нескольких слоёв:
Входные данные - аутентификация - авторизация - защита HTTP - безопасная работа с данными - секреты - логирование - аудит
Главные принципы:
Не доверять внешним данным
Проверять права на конкретное действие
Выдавать минимально необходимые привилегии
Разделять код и данные
Не хранить секреты в открытом виде
Защищать канал передачи
Использовать несколько уровней защиты
Сохранять возможность расследовать значимые действия
Когда эта модель понятна, SQL Injection, XSS, CSRF, JWT, HTTPS и другие механизмы занимают конкретное место в общей системе защиты.