Тестирование санитизации

Санитизация в библиотеке Validator.js рассматривается как процесс приведения входных строк к безопасному и предсказуемому виду перед дальнейшей обработкой. В отличие от валидации, которая отвечает за проверку корректности данных, санитизация изменяет вход, устраняя потенциально опасные или нежелательные символы и конструкции. В Validator.js для этого используются такие методы, как escape, trim, stripLow, normalizeEmail, а также ряд вспомогательных функций, применяемых к строкам.

Основная особенность тестирования санитизации заключается в том, что проверяется не факт соответствия правилам, а трансформация входных данных. Это требует более детального подхода к фиксации ожидаемых преобразований, особенно в пограничных случаях.


Поведение санитизаторов как объект тестирования

Санитизаторы в Validator.js обладают детерминированным поведением: одинаковый вход всегда приводит к одинаковому выходу. Это свойство делает их удобными для модульного тестирования. Однако сложность возникает при обработке:

  • символов Unicode;
  • HTML-специальных последовательностей;
  • управляющих символов (tab, newline, carriage return);
  • смешанных строк с валидными и невалидными фрагментами;
  • уже частично очищенных данных.

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


Тестирование escape и защита от XSS

Метод escape предназначен для экранирования HTML-специфичных символов:

  • <&lt;
  • >&gt;
  • &&amp;
  • "&quot;
  • '&#x27;

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

Пример тестового сценария:

import validator from "validator";

test("escape должен экранировать HTML символы", () => {
  const input = `<script>alert("XSS")</script>`;
  const output = validator.escape(input);

  expect(output).toBe(
    `&lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;`
  );
});

Особое внимание уделяется цепочкам вложенных конструкций:

test("escape корректно обрабатывает уже экранированные сущности", () => {
  const input = `&lt;b&gt;text&lt;/b&gt;`;
  const output = validator.escape(input);

  expect(output).toBe(`&amp;lt;b&amp;gt;text&amp;lt;/b&amp;gt;`);
});

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


Тестирование trim и работа с пробельными символами

Функция trim удаляет пробелы по краям строки, но тестирование выходит за пределы обычных ASCII-пробелов. Важны следующие категории:

  • обычные пробелы;
  • табуляции (\t);
  • переносы строк (\n, \r\n);
  • неразрывные пробелы (\u00A0);
  • смешанные комбинации.

Пример тестов:

test("trim удаляет пробелы по краям строки", () => {
  expect(validator.trim("   hello   ")).toBe("hello");
});

test("trim обрабатывает табы и переносы строк", () => {
  expect(validator.trim("\n\t hello \r\n")).toBe("hello");
});

Отдельное значение имеет тестирование строк, содержащих только пробельные символы, поскольку результатом должна быть пустая строка.


stripLow и обработка управляющих символов

Метод stripLow удаляет ASCII-контрольные символы (коды < 32 и 127). Тестирование требует проверки диапазонов значений:

test("stripLow удаляет управляющие символы", () => {
  const input = "hello\u0000world\u0008";
  const output = validator.stripLow(input);

  expect(output).toBe("helloworld");
});

Важным аспектом является режим сохранения табуляций и переносов строк, который может быть включён параметрами. Это требует параметризованных тестов:

test("stripLow сохраняет переносы строк при включенной опции", () => {
  const input = "a\nb\rc";
  const output = validator.stripLow(input, { keep_new_lines: true });

  expect(output).toBe("a\nb\rc");
});

normalizeEmail и сложные правила преобразования

Санитизация email-адресов относится к наиболее сложным случаям. Метод normalizeEmail изменяет домены и локальные части адреса в зависимости от правил почтовых провайдеров.

Основные направления тестирования:

  • приведение домена к нижнему регистру;
  • удаление точек в Gmail-адресах;
  • игнорирование плюсовых алиасов;
  • сохранение или удаление субдоменов;
  • обработка нестандартных доменов.

Пример:

test("normalizeEmail приводит Gmail к каноническому виду", () => {
  const input = "Test.Email+spam@GMAIL.com";
  const output = validator.normalizeEmail(input);

  expect(output).toBe("testemail@gmail.com");
});

Также важно тестировать отключаемые параметры:

test("normalizeEmail сохраняет точки при отключенной опции gmail_remove_dots", () => {
  const input = "t.e.s.t@gmail.com";
  const output = validator.normalizeEmail(input, {
    gmail_remove_dots: false
  });

  expect(output).toBe("t.e.s.t@gmail.com");
});

Пограничные случаи и устойчивость преобразований

Санитизация часто ломается на неожиданных входах:

  • пустые строки;
  • null и undefined (при нестрогой типизации);
  • очень длинные строки;
  • строки с смешанными кодировками;
  • эмодзи и суррогатные пары.

Пример теста на устойчивость:

test("sanitize не ломается на пустой строке", () => {
  expect(validator.escape("")).toBe("");
});

И тест на длинные входы:

test("escape корректно обрабатывает длинные строки", () => {
  const input = "a".repeat(10000);
  const output = validator.escape(input);

  expect(output.length).toBe(10000);
});

Property-based тестирование санитизации

Для Validator.js эффективно применяется property-based подход, при котором проверяются свойства функций, а не конкретные примеры.

Основные свойства:

  • результат escape никогда не содержит необработанных < или >;
  • trim не увеличивает длину строки;
  • stripLow не добавляет символы;
  • санитизаторы являются детерминированными.

Пример свойства:

test("escape не оставляет символов < и >", () => {
  fc.assert(
    fc.property(fc.string(), (str) => {
      const result = validator.escape(str);
      return !result.includes("<") && !result.includes(">");
    })
  );
});

Регрессионное тестирование изменений поведения

При обновлении Validator.js критично фиксировать старое поведение функций. Любое изменение в алгоритмах санитизации может привести к:

  • несовместимости с ранее сохранёнными данными;
  • изменению формата email;
  • ослаблению защиты от XSS;
  • различиям между средами выполнения.

Регрессионные тесты фиксируют конкретные входы и ожидаемые выходы, предотвращая непреднамеренные изменения:

test("регрессия escape для кавычек", () => {
  expect(validator.escape("\"'")).toBe("&quot;&#x27;");
});

Санитизация как часть цепочки преобразований

В реальных приложениях санитизация редко используется изолированно. Чаще она комбинируется:

const result = validator.trim(
  validator.escape(userInput)
);

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


Безопасность и тестирование как защита от уязвимостей

Основная цель тестирования санитизации заключается в предотвращении классов уязвимостей:

  • XSS (Cross-Site Scripting);
  • injection через HTML-атрибуты;
  • некорректная интерпретация email-адресов;
  • обход фильтров через Unicode-символы.

Тесты должны моделировать реальные атаки, включая комбинированные payload-строки:

test("escape блокирует XSS payload", () => {
  const input = `<img src=x oner ror=alert(1)>`;
  const output = validator.escape(input);

  expect(output.includes("onerror")).toBe(false);
});