В системах аутентификации одной из наиболее недооценённых угроз остаются атаки по времени выполнения (timing attacks). Они основаны на том, что различия во времени сравнения строк могут раскрывать информацию о секретных данных, включая хеши паролей, токены и подписи.
Стандартное сравнение строк в JavaScript выполняется посимвольно и завершается сразу при обнаружении первого различия. Это означает, что время выполнения операции зависит от совпадающего префикса строк.
Пример:
"abcdef" и "abcxyz" сравниваются быстрее,
чем"abcdef" и "abcdex"Такое поведение создаёт утечку информации: атакующий может измерять время ответа сервера и постепенно восстанавливать секрет.
В Node.js это особенно критично в контексте:
Постоянное время (constant-time) означает, что операция выполняется за одинаковое время независимо от входных данных.
Ключевая идея:
Это исключает возможность статистического анализа времени ответа.
При использовании библиотек хеширования паролей, например
password-hash, типичный поток выглядит так:
Ошибочная реализация может выглядеть так:
if (hash === storedHash) {
// доступ разрешён
}
Хотя оператор === в JavaScript кажется безопасным, на
уровне движка сравнение строк может завершаться раньше при несовпадении,
что делает поведение потенциально измеримым.
Node.js предоставляет встроенный механизм для безопасного сравнения буферов:
const crypto = require('crypto');
const a = Buffer.from('secret1');
const b = Buffer.from('secret2');
crypto.timingSafeEqual(a, b);
Buffer или
Uint8ArrayВажно учитывать проверку длины отдельно:
function safeEqual(a, b) {
const aBuf = Buffer.from(a);
const bBuf = Buffer.from(b);
if (aBuf.length !== bBuf.length) {
return false;
}
return crypto.timingSafeEqual(aBuf, bBuf);
}
При использовании password-hash и подобных библиотек
важно понимать, что сами хеши не сравниваются напрямую как строки в
критических местах.
Правильный подход:
=== для чувствительных данныхПример интеграционного слоя:
const crypto = require('crypto');
function verifyPassword(inputPassword, storedHash) {
const inputHash = hashPassword(inputPassword); // условная функция
const a = Buffer.from(inputHash);
const b = Buffer.from(storedHash);
if (a.length !== b.length) {
return false;
}
return crypto.timingSafeEqual(a, b);
}
Самая распространённая ошибка — доверие оператору строгого равенства.
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) return false;
}
Такой код не является безопасным, поскольку время выхода зависит от позиции различия.
Иногда сравниваются строки и буферы без нормализации, что приводит к непредсказуемому поведению и потенциальным утечкам через исключения.
Даже если используется сильный алгоритм хеширования (bcrypt, scrypt, argon2), проблема timing attack остаётся на этапе сравнения результатов.
Хеширование защищает данные математически, но не защищает от утечек через реализацию.
Node.js работает с бинарными данными через Buffer, и это
основной формат для безопасных сравнений.
Ключевые правила:
Bufferutf8, hex,
base64)Пример:
Buffer.from(hash, 'hex');
В реальных системах безопасная схема включает:
timingSafeEqualПример:
const crypto = require('crypto');
function verify(stored, input) {
const inputBuf = Buffer.from(input);
const storedBuf = Buffer.from(stored);
if (storedBuf.length !== inputBuf.length) {
return false;
}
return crypto.timingSafeEqual(storedBuf, inputBuf);
}
Постоянное время сравнения иногда воспринимается как более медленное решение. На практике:
В криптографических системах безопасность важнее микропроизводительности.
При использовании библиотеки password-hash важно
разделять:
password-hash отвечает за создание хеша, но безопасное
сравнение должно выполняться на уровне приложения через
криптографические инструменты Node.js.
Это особенно важно при кастомных реализациях, где разработчики вручную сравнивают строки вместо использования безопасных примитивов.
Обобщённая схема:
Эта модель применяется не только к паролям, но и к API-ключам, токенам сессий и цифровым подписям.