Сервис проверяет API-ключи обычным сравнением строк. Какую уязвимость создаёт такая реализация?
если полученный_ключ == сохранённый_ключ:
разрешить_доступ()
Обычное сравнение строк может раскрывать API-ключ по времени выполнения: при совпадении большего префикса сравнение способно работать дольше. Для проверки секретов нужен защищённый от временных атак механизм сравнения, а сами ключи следует хранить и передавать с учётом их жизненного цикла.
Криптографические операции могут непреднамеренно раскрывать сведения через измеримые побочные каналы: время выполнения, потребление памяти или особенности ошибок. Временные атаки стали практической проблемой, потому что удалённый клиент иногда способен собрать много измерений и статистически обнаружить зависимость между временем ответа и совпадающими частями секрета.
Защищённое сравнение появилось как способ уменьшить зависимость времени проверки от позиции первого несовпадающего байта. Это не заменяет остальные меры защиты, но закрывает отдельный канал утечки при сравнении секретных значений.
Если сравнение завершается на первом несовпадении, ключ с правильным первым символом может обрабатываться немного дольше ключа с неправильным первым символом. Повторяя запросы и усредняя шум сети, атакующий может поэтапно подбирать значение.
Последствия зависят от контекста: утечка короткого API-ключа может привести к несанкционированному доступу, подделке запросов или использованию платных ресурсов. Даже защищённое сравнение не спасёт систему, если endpoint не ограничивает частоту попыток, ключи слишком короткие или сервер раскрывает различия в ответах и кодах ошибок.
Секреты следует сравнивать функцией, которая обрабатывает все элементы сравниваемых значений без досрочного выхода из-за первого несовпадения. На практике используют проверенную библиотечную функцию, например:
Такая функция уменьшает зависимость времени сравнения от содержимого строк. Это не абсолютная математическая гарантия одинакового времени во всей системе: остаются сетевой шум, различия длины, планирование процессов, кэширование и другие операции вокруг сравнения. Поэтому значения обычно приводят к сопоставимой форме и дополнительно применяют rate limiting, блокировку или замедление повторных попыток, аудит и ротацию ключей.
Для API-ключей предпочтительно хранить на сервере не обратимо восстанавливаемый секрет, а его криптографический отпечаток, если архитектура не требует предъявлять исходный ключ третьей стороне. При проверке сначала находят запись по публичному идентификатору ключа, затем вычисляют отпечаток полученного секрета и сравнивают значения защищённым способом. Сравнение должно выполняться после одинаковой нормализации формата; произвольное изменение регистра или пробелов может либо сломать валидные ключи, либо расширить множество принимаемых значений.
Нельзя полагаться только на «достаточно медленный» ответ или добавлять случайную задержку как единственную защиту: задержка усложняет измерения, но не устраняет утечку и ухудшает доступность сервиса. Также следует возвращать единообразные ответы для неверного идентификатора и неверного секрета, чтобы не создавать отдельный канал перечисления существующих ключей.
Внутренний API принимает ключи партнёров. Сначала рассматривался вариант оставить обычное сравнение и добавить случайную задержку. Его плюс — простая реализация, но задержка увеличивает нагрузку и при большом числе запросов всё равно может быть устранена статистической обработкой.
Второй вариант — хранить ключи в открытом виде и использовать защищённое сравнение. Он проще для миграции, но компрометация базы сразу раскрывает все действующие ключи.
Выбран вариант с публичным идентификатором ключа, хранением сервером только отпечатка секрета, защищённым сравнением, ограничением частоты попыток и плановой ротацией. Это уменьшает последствия утечки базы и затрудняет эксплуатацию временного канала. Важный компромисс — необходимо реализовать безопасную выдачу, отзыв и замену ключей, а также оставить короткое окно перекрытия при ротации для бесшовного перехода партнёров.
1. Достаточно ли защищённого сравнения, если ключ передаётся по HTTP без шифрования?
Нет. Защищённое сравнение уменьшает локальный временной канал на сервере, но HTTP позволяет перехватить сам ключ. Передача секрета должна выполняться через защищённый транспорт, обычно TLS, а сервер должен корректно проверять сертификаты и не допускать обхода TLS на доверяемом участке.
2. Почему сравнение значений разной длины требует отдельного внимания?
Длина может раскрывать дополнительную информацию, а некоторые реализации завершаются сразу после обнаружения различия длины. Поэтому формат ключей делают фиксированной длины либо сначала приводят значения к фиксированному представлению криптографическим хешированием, а затем сравнивают отпечатки защищённой функцией. Это не отменяет необходимости проверять входной формат и ограничивать его размер.
3. Может ли защищённое сравнение заменить хеширование API-ключей?
Нет. Эти меры решают разные задачи. Защищённое сравнение снижает утечку через время выполнения, а хранение отпечатка уменьшает ущерб при компрометации базы; для высокоценного секрета дополнительно нужны TLS, контроль попыток, отзыв и ротация. Если серверу требуется восстановить исходный секрет для исходящего вызова, вместо отпечатка может понадобиться защищённое хранилище ключей, но тогда компрометация этого хранилища становится отдельным риском.