Тестирование граничных случаев: пустые строки, спецсимволы, unicode

Поведение bcrypt.js при обработке строк пароля напрямую зависит от того, как JavaScript представляет строки и как библиотека преобразует их в байты перед хешированием. На практике большинство проблем возникает не в самом алгоритме, а в некорректно учтённых граничных значениях входных данных.


Пустые строки

Пустая строка является допустимым входом с точки зрения JavaScript, но с точки зрения криптографического хеширования это отдельный крайний случай.

import bcrypt from "bcryptjs";

const hash = bcrypt.hashSync("", 10);
const result = bcrypt.compareSync("", hash);

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

Особенность заключается в том, что:

  • пустая строка не вызывает ошибок в bcrypt.js
  • результат сравнения всегда корректен
  • внешняя валидация должна блокировать такой ввод до хеширования

Строки с пробелами

Пробелы в начале, конце и внутри строки не интерпретируются библиотекой как специальные символы.

const password1 = "password";
const password2 = " password ";

Для bcrypt это разные строки, и их хеши полностью различаются.

Типичная ошибка возникает на уровне UI или API:

  • автоматическое trim() перед хешированием
  • отсутствие trim() при логине

Это приводит к несоответствию хешей при одинаковом визуальном пароле.


Специальные символы

bcrypt.js корректно работает с любыми ASCII-символами:

const password = "!@#$%^&*()_+-=[]{}|;':\",.<>/?";
const hash = bcrypt.hashSync(password, 10);

Особенности:

  • символы не экранируются вручную
  • bcrypt получает уже интерпретированную JS-строку
  • критично учитывать кодировку при передаче через HTTP/JSON

Ошибки чаще всего появляются не в bcrypt, а в слоях сериализации:

  • JSON.stringify может менять представление
  • HTML-формы могут трансформировать символы
  • URL encoding может искажать данные

Null-байт и управляющие символы

JavaScript позволяет использовать управляющие символы внутри строки:

const password = "pass\0word";

bcrypt.js не трактует \0 как терминатор строки, как это происходит в некоторых языках C.

Поведение:

  • строка хешируется полностью, включая null-байт
  • сравнение работает корректно

Проблема возникает при интеграции с системами, где null-байт считается ограничителем строки (например, некоторые C-бэкенды или старые API).


Unicode и многобайтовые символы

bcrypt.js работает с UTF-8 представлением строки. Это означает, что:

  • один символ может занимать несколько байт
  • ограничение bcrypt по длине относится к байтам, а не к символам
const password = "пароль?密码";
const hash = bcrypt.hashSync(password, 10);

Особенности:

  • эмодзи и иероглифы увеличивают размер в байтах
  • визуально короткие строки могут превышать лимит bcrypt

Критический лимит 72 байта

bcrypt использует ограничение на входную строку: учитываются только первые 72 байта.

Это один из самых важных граничных случаев:

const password = "a".repeat(1000);

Поведение:

  • только первые 72 байта участвуют в хешировании
  • остальная часть строки игнорируется
  • разные пароли могут давать одинаковый хеш

Это приводит к уязвимости:

  • "password123" и "password123AAAAAAAAAAAA..." (длинная версия) могут быть эквивалентны

При тестировании важно учитывать:

  • длину именно в байтах (UTF-8)
  • наличие Unicode символов, увеличивающих размер

Нормализация Unicode

Unicode строки могут иметь разные представления одного и того же символа:

  • NFC (составная форма)
  • NFD (разложенная форма)
const a = "é";       // NFC
const b = "e\u0301"; // NFD

bcrypt.js не выполняет нормализацию автоматически.

Результат:

  • строки визуально одинаковые
  • хеши различаются

Это критично при:

  • вводе с разных клавиатур
  • копировании из внешних источников
  • обработке данных с мобильных устройств

Экранирование и JSON-передача

При передаче паролей через API часто возникает проблема преобразования строк:

fetch("/api/login", {
  method: "POST",
  body: JSON.stringify({ password: "pa\"ss\\word" })
});

Ошибки возникают не в bcrypt.js, а в промежуточных слоях:

  • некорректное декодирование JSON
  • двойное экранирование
  • потеря обратных слэшей

Важно учитывать, что bcrypt получает уже восстановленную строку, и любые изменения до этого этапа влияют на итоговый хеш.


Тестирование граничных случаев

Проверка поведения bcrypt.js обычно включает набор типовых сценариев:

Пустые и минимальные строки

expect(bcrypt.compareSync("", bcrypt.hashSync("", 10))).toBe(true);
expect(bcrypt.compareSync("a", bcrypt.hashSync("a", 10))).toBe(true);

Unicode строки

const password = "测试?пароль";
const hash = bcrypt.hashSync(password, 10);

expect(bcrypt.compareSync(password, hash)).toBe(true);

Проверка 72-байтового ограничения

const longPassword = "a".repeat(200);
const hash = bcrypt.hashSync(longPassword, 10);

const modified = "a".repeat(200) + "b";
const hash2 = bcrypt.hashSync(modified, 10);

bcrypt.compareSync(longPassword, hash) === bcrypt.compareSync(modified, hash);

Одинаковый результат при значительном отличии строк указывает на достижение лимита.


Фаззинг входных данных

Для выявления скрытых проблем применяются случайные генерации строк:

  • случайные Unicode последовательности
  • комбинации управляющих символов
  • строки с разной нормализацией
function randomString(len) {
  return Array.from({ length: len }, () =>
    String.fromCharCode(Math.floor(Math.random() * 0x10FFFF))
  ).join("");
}

Такие тесты позволяют выявлять:

  • несогласованность кодировок
  • различия между платформами
  • проблемы сериализации

Поведение при ошибках и нестандартных типах

bcrypt.js ожидает строку. При передаче других типов происходит неявное преобразование:

bcrypt.hashSync(null);       // "null"
bcrypt.hashSync(undefined);  // "undefined"
bcrypt.hashSync(12345);      // "12345"

Это создаёт скрытый класс ошибок:

  • отсутствие строгой типизации входа
  • неожиданные хеши при ошибках API
  • утечки логики преобразования типов

Сравнение и устойчивость результата

Функция сравнения:

bcrypt.compareSync(password, hash);

устойчива к:

  • различиям соли
  • различиям внутреннего представления хеша

но чувствительна к:

  • байтовому представлению строки
  • нормализации Unicode
  • усечению по 72 байта
  • любым изменениям до этапа хеширования