Безопасность в вебе: как защитить приложение без паранойи и перегруза кода

Многие разработчики относятся к безопасности либо как к «черной магии», которой занимаются отдельные люди из отдела ИБ, либо как к бесконечному списку запретов, которые только тормозят разработку. В итоге мы получаем два крайности: либо приложение, которое «дырявое», как швейцарский сыр, либо перегруженный костылями монстр, где проверка одного поля ввода занимает 40 строк кода и делает поддержку проекта невыносимой.
Давайте честно: абсолютной безопасности не существует. Любая система может быть взломана, если у атакующего достаточно времени и ресурсов. Наша задача — не построить неприступную крепость (это дорого и бессмысленно), а сделать стоимость атаки выше, чем потенциальная выгода от неё.
Как добиться этого, не превращая код в помойку из проверок? Разберем прагматичный подход.
1. Смена парадигмы: от «черных списков» к «белым»
Типичная ошибка новичка — пытаться фильтровать «плохие» символы. Вы создаете регулярное выражение, которое ищет слово DROP TABLE или тег <script>, но хакеры используют обфускацию, кодировки или новые векторы атак, которые вы просто не предусмотрели.
Правило простое: запрещайте всё, что не разрешено явно.
Если вы ждете от пользователя возраст, разрешите только цифры. Если ждете имя пользователя — латиницу, кириллицу и подчеркивание. Всё остальное должно отсекаться на входе без попыток «почистить» или «исправить» строку. Это избавляет вас от необходимости писать сотни условий и делает код предсказуемым.
2. Борьба с классикой: SQL-инъекции и XSS
Эти уязвимости живут десятилетиями, но всё еще встречаются в каждом втором проекте. Причина в том, что разработчики пытаются писать свои функции экранирования.
Как не перегрузить код:
- Параметризованные запросы (Prepared Statements). Забудьте про конкатенацию строк в SQL. Используйте ORM или стандартные механизмы драйверов БД. Когда вы передаете данные отдельным параметром, база данных воспринимает их как данные, а не как часть исполняемого кода. Это закрывает вопрос SQL-инъекций на 99%.
- Автоматическое экранирование в шаблонизаторах. Современные движки (Jinja2, Blade, React, Vue) по умолчанию экранируют вывод. Не отключайте это ради одной «красивой» ссылки с HTML-тегом. Если вам действительно нужно вывести HTML, используйте специализированные библиотеки для санитайзинга (например, DOMPurify), а не пытайтесь написать свою функцию
replace('<', '<').
3. Аутентификация и сессии: не изобретайте велосипед
Самописная система авторизации — это кратчайший путь к утечке базы данных. Хранение паролей в открытом виде или использование MD5/SHA-1 в 2024 году — это профессиональный грех.
Чек-лист для спокойного сна:
- Хеширование с солью. Используйте Argon2 или bcrypt. Эти алгоритмы специально сделаны «медленными», чтобы перебор паролей (брутфорс) стал экономически невыгодным.
- JWT vs Сессии. Если вы используете JSON Web Tokens, не храните в них чувствительную информацию и обязательно настройте короткий срок жизни (exp) в сочетании с Refresh-токенами.
- HttpOnly и Secure флаги. Куки, в которых лежат токены, должны иметь флаг
HttpOnly(чтобы JavaScript не мог их украсть через XSS) иSecure(чтобы они передавались только по HTTPS). Это две галочки в конфиге, которые закрывают огромную дыру в безопасности.
4. API и бизнес-логика: где нас обманывают чаще всего
Техническая защита (SSL, хеширование) — это база. Но большинство реальных взломов происходит на уровне логики. Например, когда пользователь меняет id в URL с /api/user/123/profile на /api/user/124/profile и видит чужие данные. Это называется IDOR (Insecure Direct Object Reference).
Как защититься без раздувания кода:
-
- Проверка владения ресурсом. Вместо того чтобы просто искать запись по ID, добавляйте в запрос условие принадлежности:
SELECT * FROM orders WHERE id = :id AND user_id = :current_user_id.
- Проверка владения ресурсом. Вместо того чтобы просто искать запись по ID, добавляйте в запрос условие принадлежности:
-
- Валидация типов на уровне DTO. Используйте типизацию и схемы валидации (например, Pydantic в Python или Zod в TypeScript). Это позволяет вынести всю «грязную» проверку типов из бизнес-логики в один слой на входе. Код контроллеров станет чище, а безопасность — системнее.
5. Прагматичный подход к инфраструктуре
Безопасность приложения бесполезна, если сервер настроен «по умолчанию». Но и превращать сервер в бункер с тремя уровнями фаерволов не всегда нужно.
Минимальный набор действий:
-
- HTTPS везде. Let’s Encrypt сделал это бесплатным. Нет причин использовать HTTP даже на внутренних ресурсах.
-
- Заголовки безопасности (Security Headers). Добавьте в ответы сервера заголовки
Content-Security-Policy(CSP),X-Content-Type-Options: nosniffиX-Frame-Options: DENY. Это простые настройки веб-сервера (Nginx/Apache), которые не требуют изменения одной строчки вашего кода, но блокируют целый класс атак.
- Заголовки безопасности (Security Headers). Добавьте в ответы сервера заголовки
-
- Принцип наименьших привилегий. Ваше приложение не должно подключаться к базе данных под пользователем
rootилиadmin. Создайте отдельного пользователя с правами только наSELECT,INSERT,UPDATEдля конкретной базы.
- Принцип наименьших привилегий. Ваше приложение не должно подключаться к базе данных под пользователем
Итог: формула здорового баланса
Чтобы не сойти с ума от паранойи, следуйте правилу: автоматизируйте всё, что можно, и делегируйте стандартным инструментам.
-
- Не пишите свои фильтры $\rightarrow$ используйте валидаторы.
-
- Не пишите свои запросы $\rightarrow$ используйте параметризацию.
-
- Не пишите свои методы шифрования $\rightarrow$ используйте проверенные библиотеки.
Безопасность — это не разовое действие, а гигиена. Если вы внедрили эти базовые вещи, ваше приложение уже будет защищено лучше, чем 80% проектов в сети. Остальное — это вопрос масштаба вашего бизнеса и стоимости данных, которыми вы владеете. В большинстве случаев «достаточной» защиты вполне хватает, чтобы спать спокойно и писать чистый, поддерживаемый код.

