Не доверяй вводу: откуда приходят данные

«Внешним» считается всё, что вы не вычислили сами внутри доверенной зоны.

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

Почему это сквозная тема

Подавляющее большинство уязвимостей — инъекции, XSS, path traversal, переполнения — начинаются с одного: программа приняла внешние данные и обошлась с ними так, будто они безопасны. Если усвоить ровно одну идею из курса, пусть это будет «не доверяй вводу».

Чтобы понять, почему эта тема сквозная, посмотрите на неё глазами атакующего. Атакующий не видит ваш красивый код и архитектуру — он видит входные точки: поля, параметры, заголовки, загрузку файлов. Каждая такая точка — это контролируемый им «рычаг», которым он пробует пошатать систему. Он не обязан играть по вашим правилам: туда, где вы ожидаете число от 1 до 100, он пришлёт строку длиной в мегабайт; туда, где ждёте имя, — управляющие символы. Безопасность приложения во многом сводится к тому, насколько строго каждая входная точка обрабатывает «не то, что ожидалось».

Важно и то, что доверие — свойство не данных, а их происхождения. Одна и та же строка admin безопасна, если вы записали её в код как константу, и опасна, если она пришла из формы логина: во втором случае вы не знаете, что ещё могло приехать рядом. Поэтому граница доверия проходит не по содержанию, а по источнику: всё, что пересекло границу снаружи внутрь, считается враждебным, пока не доказано обратное явной проверкой.

Что такое «граница доверия»

Граница доверия — это воображаемая линия между зоной, которую вы контролируете (ваш процесс, ваша память, ваши константы), и всем остальным миром. Сетевой запрос, чтение файла, ответ стороннего API, чтение из очереди сообщений — каждое такое действие переносит данные через границу. Хорошая привычка — на code review мысленно рисовать эти линии и спрашивать у каждого пересечения: «провалидированы ли данные ровно здесь, прежде чем я начал им доверять?» Чем раньше вы поставите проверку у самой границы, тем меньше кода вниз по потоку придётся считать «потенциально заражённым».

Ввод — это не только поля формы

Новички думают про ввод как про текст в форме. На деле недоверенных источников куда больше:

  • тело и query-параметры HTTP-запроса;
  • HTTP-заголовки (включая User-Agent, Referer, Host, Cookie);
  • пути и имена загружаемых файлов, их содержимое;
  • данные из БД, если их туда положил пользователь раньше (хранимые атаки);
  • ответы внешних API и вебхуки;
  • переменные окружения и аргументы командной строки в недоверенном контексте.

Опасный пример — заголовок Host, которому часто слепо доверяют при формировании ссылок в письмах:

// Уязвимо: ссылку для сброса пароля строим из заголовка Host,
// который контролирует клиент -> ссылка может вести на чужой домен
link = "https://" + request.headers["Host"] + "/reset?token=" + token;

// Безопасно: домен берём из доверенной конфигурации сервера
link = config.BASE_URL + "/reset?token=" + token;

Хранимые данные тоже бывают враждебны

Распространённое заблуждение — «данные из нашей БД уже доверенные». Но если их туда внёс пользователь (имя профиля, комментарий), они так же опасны при выводе. Так возникает хранимый XSS: вредоносная строка лежит в базе и срабатывает у каждого, кто открывает страницу.

Опасность хранимых данных коварнее, чем у прямого ввода, по двум причинам. Во-первых, между моментом записи и моментом срабатывания может пройти много времени, и связь между «кто-то отредактировал профиль» и «у поддержки в админке что-то сломалось» неочевидна — атака как бы «спит». Во-вторых, она бьёт не по автору ввода, а по всем, кто потом этот ввод увидит: по другим пользователям, по администратору в панели, по системе аналитики, импортирующей данные. Один заражённый комментарий способен затронуть тысячи просмотров. Поэтому правило простое: данные не становятся доверенными от того, что полежали в базе. Доверие определяется происхождением, а происхождение — пользователь.

Тот же эффект встречается и за пределами веб-страниц. Имя файла, сохранённое пользователем, позже подставляется в путь; описание товара уходит в генератор PDF; никнейм попадает в лог, который читает другой инструмент. Каждый такой «потребитель» хранимых данных — это новая точка, где забытое экранирование снова делает строку опасной.

Как работает под капотом: tainted data

Полезно мысленно «помечать» внешние данные как tainted (заражённые). Заражённость «течёт» по программе: если из tainted-строки склеить SQL — запрос заражён; если вывести в HTML без экранирования — страница заражена. Снять метку можно только осознанным шагом: валидацией или контекстным экранированием. Некоторые языки и линтеры умеют отслеживать taint автоматически.

ввод (tainted)
   |
   v
 [валидация / экранирование] -- снимает «заражённость»
   |
   v
 безопасное использование (запрос, вывод, путь)

Полезно держать в голове, что taint не «исчезает сам по себе» при копировании или склейке. Если вы вырезали из заражённой строки подстроку, она тоже заражена; если склеили заражённую строку с доверенной, результат заражён целиком. Метка снимается только осознанным шагом обработки в правильном месте, и снимается она под конкретный приёмник: строка, обезвреженная для HTML, не считается обезвреженной для SQL. Именно поэтому taint удобно мыслить не как «грязная/чистая», а как «грязная» либо «чистая для такого-то контекста».

Эта модель объясняет и типичный антипаттерн «глобальной чистки на входе». Кажется заманчивым один раз пропустить весь ввод через универсальный фильтр и дальше ни о чём не думать. Но универсальной чистоты не бывает: фильтр под HTML испортит данные для SQL, фильтр под shell сломает легитимный текст. Taint-модель подсказывает правильную стратегию — нести «заражённость» как можно дальше по коду и снимать её точечно у каждого приёмника, а не размазывать одну грубую чистку по входу.

Частые ошибки

  • Доверять заголовкам. Host, X-Forwarded-For, Referer подделываются тривиально.
  • Считать БД доверенной. Пользовательские данные опасны и на чтении.
  • Доверять «внутренним» сервисам. В микросервисах внутренний вызов тоже может нести чужой ввод.
  • Доверять скрытым и неизменяемым полям формы. hidden-поля, disabled-элементы и значения из localStorage приходят в запросе обычным текстом и так же подделываются.
  • Считать клиентскую валидацию защитой. Проверка в браузере — это удобство для пользователя; запрос можно отправить и в обход формы, поэтому серверная проверка обязательна.

Итоги

  • Недоверенный ввод — всё из-за границы доверия, а не только поля формы.
  • Заголовки, файлы, ответы API, хранимые пользовательские данные — тоже ввод.
  • Мысленно помечайте внешние данные как tainted и снимайте метку только валидацией/экранированием.
Проверьте себя
1. Почему заголовку HTTP Host нельзя доверять при формировании ссылок?
AОн всегда пустой
BЕго значение контролирует клиент и может быть подделано, уведя ссылку на чужой домен
CОн доступен только по HTTPS
DБраузеры его не отправляют
2. Что такое хранимый (stored) XSS с точки зрения доверия к данным?
AАтака на саму базу данных
BВредоносные данные, ранее внесённые пользователем, хранятся в БД и срабатывают при выводе другим пользователям
CОшибка шифрования диска
DУтечка резервной копии
3. Что описывает концепция tainted data?
AДанные, повреждённые на диске
BВнешние данные «помечены как заражённые», и метка течёт по программе, пока её не снимут валидацией или экранированием
CДанные, зашифрованные неправильным ключом
DЛюбые числа с плавающей точкой