Экспресс-курс · No. 22
Почти любая веб-уязвимость восходит к одной ошибке: доверию данным, что пришли снаружи. Атакующий управляет запросом, формой, URL, загруженным файлом — всем. Веб-безопасность — это дисциплина относиться к каждому клочку внешнего ввода как к враждебному, пока не доказано обратное, и выучить горстку классических атак, что наказывают любого, кто забыл.
Только суть · Один образ на идею · Выучи слова
До любой конкретной атаки есть единственный принцип, что предотвращает большинство из них. Впитай эту одну мысль — и остаток курса окажется просто ею, применённой в разных местах.
Любой внешний ввод управляется атакующим
Пограничный пост относится к каждой прибывающей посылке как к потенциально опасной, пока не досмотрит, — он не пропускает вещи только потому, что они выглядят обычно.
Всё, что входит в твою систему снаружи — поля форм, URL, заголовки, загруженные файлы, вызовы API, — управляется тем, кто это прислал, а это может быть атакующий. Он может прислать что угодно, а не только то, что задумала твоя форма. Так что фундаментальное правило: считай любой внешний ввод враждебным, пока не провалидировал его. Почти любая уязвимость в курсе — это, по сути, место, где кто-то доверился вводу, которому не следовало.
Граница доверия — там, где живут проверки
Стена вокруг здания с одними охраняемыми воротами — внутри доверено, снаружи нет, и всё, что пересекает внутрь, проверяется на этой черте.
Граница доверия (trust boundary) — это черта между внешним миром и твоей системой. Данные, пересекающие внутрь, надо проверять на этой границе, потому что, оказавшись внутри, твой код склонен считать их безопасными. Классическая ошибка — валидировать на клиенте (в браузере) и считать, что этого хватит, — но атакующий обходит браузер целиком. Настоящие проверки безопасности случаются на сервере, на границе, которой ты реально управляешь.
Валидируй ввод, экранируй вывод
Вышибала проверяет документы на входе (кто заходит), а переводчик следит, чтобы твои слова не прочли как команду в соседней комнате (как вещи выходят). Две разные работы.
Две привычки останавливают большинство атак. Валидируй ввод: отвергай данные не той формы, что ты ждёшь, — неверный тип, слишком длинные, кривые. Экранируй вывод: когда кладёшь данные в другой контекст (запрос к базе, HTML-страницу, команду шелла), нейтрализуй всё, что там можно прочесть как код. Почти любая атака ниже — это недоверенный ввод, проскальзывающий в место, где его интерпретируют как инструкции, а не как данные.
Каждый клочок внешнего ввода управляется атакующим. Валидируй его на границе на сервере и экранируй везде, где используешь. Это одно правило предотвращает большинство атак.
Самый разрушительный класс атак и самый простой для понимания: недоверенный ввод трактуется как код. Хрестоматийный случай — SQL-инъекция, и она показывает весь паттерн.
Инъекция: ввод становится кодом
Форма просит твоё имя, а кто-то пишет вместо него инструкцию — и клерк, читая всю строку вслух, случайно выполняет инструкцию.
Инъекция (injection) случается, когда недоверенный ввод подмешивают в команду или запрос, и он выполняется как часть его. Система собиралась использовать твой ввод как данные, но ввод был выкроен, чтобы его прочли как код. Это один и тот же корневой изъян через SQL, команды шелла и прочее: граница между «инструкцией» и «данными пользователя» так и не была насаждена, так что атакующий поставил инструкции.
SQL-инъекция, классический случай
Библиотечный бланк запроса, где в поле названия кто-то пишет «...а ещё отопри все двери» — и автоматическая система послушно делает оба.
В SQL-инъекции (SQL injection) атакующий печатает синтаксис базы в обычное поле. Если твой код строит запрос, склеивая ввод в строку, их ввод становится частью запроса — позволяя читать данные других пользователей, обойти вход или удалить таблицы. Это десятилетиями была одна из самых частых и серьёзных веб-уязвимостей, и идёт она целиком от построения запросов конкатенацией строк с недоверенным вводом.
Параметризованные запросы — лекарство
Форма с запертыми, помеченными ящиками: что бы ты ни написал в ящик «имя», это трактуется только как имя, никогда как часть инструкций формы, что бы ты туда ни вложил.
Лекарство — параметризованный запрос (parameterized query, prepared statement): ты пишешь запрос с заглушками и передаёшь пользовательский ввод отдельно, так что база всегда трактует его как чистые данные, никогда как SQL. Ввод может быть мерзейшей строкой, какую только вообразить, и всё равно не сможет изменить структуру запроса. Никогда не строй запрос конкатенацией ввода в строку — используй параметры, каждый раз, и SQL-инъекция попросту не сможет случиться.
Инъекция — это недоверенный ввод, выполненный как код. SQL-инъекция — классический случай, а параметризованные запросы — ввод как данные, не конкатенация — это лекарство.
Если инъекция — это ввод, становящийся кодом на сервере, то межсайтовый скриптинг — это ввод, становящийся кодом в браузере. Он превращает твою же страницу в оружие против твоих пользователей.
XSS: ввод становится скриптом в браузере
Доска объявлений сообщества, где кто-то прикалывает записку, подстроенную так, что у каждого, кто её читает, тихо обчищают карманы, — опасность доставлена самой доверенной доской.
Межсайтовый скриптинг (cross-site scripting, XSS) — это когда атакующий заставляет свой JavaScript выполниться в браузере другого пользователя, на твоём сайте. Если ты берёшь пользовательский ввод — комментарий, имя, профиль — и кладёшь его прямо на страницу, атакующий может подать <script> вместо текста, и тот выполнится у каждого, кто эту страницу смотрит. Их код теперь действует с сессией твоего пользователя: крадёт cookie, выдаёт себя за него, портит страницу.
Он работает с доверием твоего пользователя
Самозванец в доверенной форме: люди подчиняются, потому что форма — домен твоего сайта — это то, чему они доверяют, а не человек внутри неё.
XSS опасен, потому что вредоносный скрипт работает как твой сайт, со всем доверием, что пользователь даёт твоему домену. Он может читать то, что видит пользователь, действовать как он и слать его данные атакующему. У браузера нет способа узнать, что скрипт не твой, — он пришёл с твоей страницы. Поэтому «это всего лишь поле комментария» — знаменитые последние слова: любое место, где пользовательский ввод доходит до страницы, — потенциальная дыра XSS.
Экранируй вывод и используй content policy
Переводчик, что выводит слова каждого гостя как обычный текст на экране, — так что, даже если кто-то выкрикивает команду, она появляется как безобидные слова в кавычках, а не как приказ, по которому кто-то действует.
Ключевое лекарство — экранировать вывод: когда кладёшь пользовательские данные в HTML, преврати символы вроде < и > в безобидный отображаемый текст, чтобы они рендерились как контент, никогда не выполнялись как разметка. Современные фреймворки делают это по умолчанию — поэтому стоит им довериться. Наложи сверху Content Security Policy — правило браузера, что ограничивает, какие скрипты могут выполняться, — как вторую линию защиты. Относись ко всему, что доходит до страницы, как к недоверенному и нейтрализуй на выходе.
XSS — это недоверенный ввод, работающий как скрипт в браузерах твоих пользователей, с доверием твоего сайта. Экранируй всё, что доходит до страницы, и добавь Content Security Policy.
Следующая атака ничего не инъецирует — она злоупотребляет собственной привычкой браузера прикреплять твои учётные данные к каждому запросу, обманом заставляя его действовать от твоего имени без твоего намерения.
CSRF: твой браузер обманом заставляют действовать
Кто-то подделывает твою подпись на форме и шлёт её с твоего адреса — банк видит валидный, подписанный запрос от тебя и обрабатывает его, не зная, что ты его не писал.
Межсайтовая подделка запроса (cross-site request forgery, CSRF) эксплуатирует тот факт, что браузеры автоматически прикрепляют твои cookie — включая твою залогиненную сессию — к любому запросу к сайту. Атакующий кладёт скрытый запрос к твоему банку на свою вредоносную страницу; когда ты заходишь на неё, залогиненный в банк, твой браузер шлёт запрос с прикреплённой твоей сессией, и банк думает, что ты этого хотел. Тебя заставляют действовать, хотя ты этого ни разу не выбирал.
Проблема запутанного посредника
Доверенный ассистент с ключами, обманутый поддельной запиской отпереть хранилище, — у ассистента была власть, и его одурачили использовать её для кого-то другого.
CSRF — это атака «запутанного посредника» (confused deputy): твой браузер держит реальную власть (твою сессию) и обманом вынужден применить её для атакующего. Сервер не может отличить, потому что запрос выглядит ровно как настоящий — те же cookie, тот же пользователь. Изъян не в украденных учётных данных; он в том, что валидный запрос был запущен кем-то кроме тебя, и ничто не доказало твоё намерение.
Доказывай намерение токенами и SameSite
Банк, что требует одноразовый код, напечатанный только на твоей выписке, с каждым переводом, — поддельный запрос откуда-то ещё не может его включить, так что он отвергается.
Лекарство — требовать доказательство, что запрос сделала твоя страница. CSRF-токен — это секретное значение, что твой сайт встраивает в свои формы и проверяет при отправке; страница атакующего его знать не может, так что поддельные запросы проваливаются. Современное дополнение — атрибут cookie SameSite, что велит браузеру не слать твою сессионную cookie на запросы, приходящие с других сайтов. Вместе они гарантируют, что чувствительное действие пришло с твоего сайта, с твоим намерением.
CSRF обманом заставляет твой браузер выстрелить запрос с прикреплённой твоей сессией. Доказывай намерение CSRF-токеном и SameSite-cookie, чтобы поддельные межсайтовые запросы отвергались.
Некоторые данные так чувствительны, что то, как ты их хранишь, само по себе решение безопасности. Пароли — классический пример, и они вскрывают разницу между двумя словами, что люди постоянно путают.
Никогда не храни пароли открытым текстом
Держать список ключей от всех домов в незапертом ящике на ресепшене — один взлом, и каждый дом открыт. Удобство и есть катастрофа.
Если ты хранишь пароли читаемым текстом, то любой, кто добрался до твоей базы — через утечку, слив, инсайдера, — мгновенно имеет пароль каждого пользователя. А поскольку люди переиспользуют пароли, ты скомпрометировал и их другие аккаунты. Хранить пароль открытым текстом — среди самых серьёзных и базовых ошибок веб-безопасности. Данные слишком опасны, чтобы держать их в форме, что ты можешь прочесть.
Хеширование одностороннее; шифрование двустороннее
Шредер против запертого ящика: ящик можно отпереть назад к оригиналу, но шредер превращает бумагу в конфетти, что не собрать обратно, — и при этом одна и та же бумага всегда шредится одинаково.
Вот различие, что надо пришпилить. Шифрование (encryption) обратимо: с ключом ты превращаешь перемешанные данные назад в оригинал (используй для данных, что должен читать снова, вроде хранимого API-ключа). Хеширование (hashing) одностороннее: оно превращает ввод в фиксированный отпечаток, что нельзя обратить назад к вводу, но один и тот же ввод всегда даёт один отпечаток. Они решают разные задачи, и путать их — классическая ошибка.
Хешируй пароли, с солью
Проверка восковой печати: тебе не нужно оригинальное письмо, чтобы её проверить, — ты просто заново штампуешь и сравниваешь оттиски. Ты подтверждаешь совпадение, ни разу не храня сам секрет.
Ты хешируешь пароли, потому что тебе никогда не нужен пароль назад — тебе нужно только его проверить. Храни хеш; при входе хешируй то, что они напечатали, и сравни. Утечка сольёт отпечатки, а не пароли. Добавь соль (salt) — уникальное случайное значение на каждый пароль — чтобы одинаковые пароли получали разные хеши, а предвычисленные таблицы атак провалились. Используй медленный, специально созданный хеш паролей (вроде bcrypt или Argon2), никогда не быстрый общий, чтобы подбор был дорог.
Никогда не храни пароли открытым текстом. Шифрование обратимо для данных, что надо читать назад; хеширование одностороннее для данных, что только проверяешь, — хешируй пароли, с солью, медленным алгоритмом.
За конкретными атаками лежат два принципа, что ограничивают ущерб, когда что-то прорывается, — потому что в безопасности ты предполагаешь, что рано или поздно прорвётся.
Дай каждой части наименьшие привилегии, что ей нужны
Гостиничная карта-ключ, что открывает только твой номер и спортзал, — не каждую дверь в здании. Если она потеряна, ущерб ограничен тем, до чего она доставала.
Наименьшие привилегии (least privilege) значат, что каждый пользователь, сервис и учётка получают только минимум доступа, нужный для их работы, — ничего больше. Учётка базы, что использует твоё веб-приложение, не должна мочь удалять таблицы, если она лишь читает и пишет строки. Тогда, когда что-то скомпрометировано — а предполагай, что будет, — радиус взрыва мал, ограничен тем, что той одной части было позволено. Слишком широкие права превращают маленький прорыв в тотальный.
Защита вглубь: слои, а не одна стена
Замок со рвом, стеной, воротами и стражей — ни один барьер не считается идеальным, так что каждый ловит то, что пропустил прошлый.
Защита вглубь (defense in depth) значит наслаивать независимые защиты так, чтобы один отказ не был фатальным. Валидируй ввод и используй параметризованные запросы и работай на наименьших привилегиях и экранируй вывод. Любой одиночный контроль может отказать или быть обойдён; вместе они делают так, что полная компрометация требует побить их все разом. Безопасность — не одна идеальная стена, а перекрывающиеся обычные контроли, каждый из которых предполагает, что другие могут отказать.
Не выдавай подсказок в ошибках и ответах
Запертая дверь, что, когда её дёргают, услужливо объявляет «не тот ключ — настоящий латунный и слегка погнутый», — говоря взломщику ровно, как войти.
Многословные ошибки и переделящиеся ответы вручают атакующему карту. Вход, что говорит «неверный пароль» (против «нет такого пользователя»), подтверждает, какие аккаунты существуют; стек-трейс вскрывает твой фреймворк, версии и структуру запросов. Показывай пользователям общее сообщение, логируй детали приватно и никогда не возвращай больше данных, чем нужно клиенту. Тихий сбой не просто опрятен — он лишает атакующего разведки, что делает следующий шаг лёгким.
Предполагай, что что-то прорвётся. Наименьшие привилегии ограничивают ущерб, защита вглубь означает, что ни один отказ не фатален, а тихие ошибки лишают атакующего карты.
Безопасность — это практика, вплетённая в то, как ты строишь, а не фича, прикрученная в конце. Привычек немного, и большинство из них — то самое одно правило, применённое с дисциплиной.
Опирайся на фреймворк и держи его пропатченным
Ты не куёшь свои замки — ты ставишь проверенные и меняешь их, когда объявлен изъян. Городить своё — это как получить замок похуже.
Большинство классических атак уже решены зрелыми фреймворками и библиотеками: они экранируют вывод, параметризуют запросы и обрабатывают CSRF-токены за тебя — если ты используешь их как задумано, а не обходишь. А поскольку уязвимости находят в зависимостях постоянно, держи их обновлёнными; непропатченная библиотека — один из самых частых путей внутрь. Не изобретай свою криптографию или своё экранирование — используй проверенные инструменты и держи их свежими.
Знай OWASP Top 10 как свою карту
Предполётный чек-лист существует, потому что одни и те же несколько сбоев вызывают большинство крушений, — ты проверяешь их каждый раз, а не переоткрываешь в воздухе.
Не надо воображать каждую угрозу. OWASP Top 10 — это отраслевой список самых частых, серьёзных веб-уязвимостей — инъекция, сломанный контроль доступа и прочее, — и он работает как чек-лист. Пройди своё приложение против него, и поймаешь категории, что вызывают подавляющее большинство реальных прорывов. Использовать известную карту лучше, чем надеяться, что ты сам обо всём подумал.
- Где входит внешний ввод, и валидирован ли он на границе на сервере? - Запросы — параметризованы, никогда не построены конкатенацией ввода? - Вывод на страницу — экранирован, чтобы пользовательский ввод не выполнился как скрипт? - Действия, меняющие состояние — защищены от CSRF токенами и SameSite? - Чувствительные данные — пароли хешированы с солью, секреты никогда открытым текстом? - Наименьшие привилегии — есть ли у каждой части только тот доступ, что ей нужен?
- trust boundary / валидируй ввод / экранируй вывод — одно правило и две его привычки. - injection / SQL injection / параметризованный запрос — ввод, выполненный как код, и лекарство.
- XSS / Content Security Policy — ввод, выполненный как скрипт в браузере, и его защита. - CSRF / CSRF-токен / SameSite — поддельный запрос с твоей сессией и как доказать намерение. - хеширование / шифрование / соль — односторонняя проверка против двустороннего чтения и случайность на пароль. - наименьшие привилегии / защита вглубь — ограничить ущерб и наслоить защиты. - OWASP Top 10 — отраслевая карта самых частых уязвимостей.
- Ты относишься к любому внешнему вводу как к враждебному, проверяемому на сервере на границе.
- Запросы параметризованы, а вывод на страницу экранирован — по умолчанию, через фреймворк. - Меняющие состояние запросы несут CSRF-токен, а cookie — SameSite. - Пароли хешированы и посолены; ничто чувствительное не хранится открытым текстом. - Каждая часть работает на наименьших привилегиях, зависимости пропатчены, и ты проверяешься против OWASP Top 10.
Веб-безопасность — это в основном одно правило, применённое с дисциплиной: никогда не доверяй вводу. Валидируй на границе, экранируй на выходе, храни секреты безопасно и наслаивай защиты на случай, если что-то проскользнёт.