Санитизация пользовательского ввода

Санитизация пользовательского ввода — процесс очистки, нормализации и фильтрации данных, поступающих от пользователя, перед их отображением, сохранением или передачей в приложение. В контексте работы с библиотекой Choices.js эта тема особенно важна, поскольку библиотека активно взаимодействует с текстовыми значениями, HTML-разметкой, пользовательскими тегами, элементами поиска и динамически создаваемыми DOM-узлами.

Без корректной санитизации приложение становится уязвимым для:

  • XSS-атак;
  • внедрения вредоносного HTML;
  • DOM-инъекций;
  • подмены структуры интерфейса;
  • утечек данных;
  • выполнения произвольного JavaScript-кода.

Особенно критична защита в следующих сценариях:

  • пользователь может создавать собственные теги;
  • значения подгружаются с сервера;
  • используется HTML внутри options;
  • включён пользовательский рендеринг шаблонов;
  • данные отображаются повторно после сохранения.

Источники пользовательского ввода в Choices.js

Библиотека получает данные из нескольких источников:

Поле ввода

<input type="text">

или:

<select>
  <option>...</option>
</select>

Динамическая загрузка данных

choices.setChoices(data, 'value', 'label', true);

Пользовательские теги

new Choices(element, {
  addItems: true,
  addChoices: true
});

Callback-функции

callbackOnCreateTemplates

API и AJAX

fetch('/api/tags')

Основные угрозы безопасности

Внедрение HTML

Опасный ввод:

<script>alert('XSS')</script>

или:

<img src=x oner ror=alert(1)>

Если такие значения будут вставлены через innerHTML, произойдёт выполнение вредоносного кода.


DOM XSS

Уязвимость возникает при прямой вставке пользовательских данных в DOM:

element.innerHTML = userInput;

Инъекция атрибутов

Опасное значение:

" oncl ick="alert(1)

может нарушить структуру HTML.


Вредоносные URL

Пример:

jav * ascript:alert(1)

Особенности безопасности Choices.js

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

Choices.js:

  • экранирует часть HTML;
  • поддерживает текстовый рендеринг;
  • позволяет отключить HTML;
  • даёт доступ к кастомным шаблонам;
  • не фильтрует данные на сервере;
  • не предотвращает логические уязвимости.

Поэтому безопасность должна обеспечиваться на нескольких уровнях:

  1. клиент;
  2. библиотека;
  3. сервер;
  4. база данных;
  5. DOM;
  6. шаблонизатор.

Отключение HTML

Наиболее безопасный подход — полный отказ от HTML в пользовательских данных.

Использование allowHTML

const choices = new Choices(element, {
  allowHTML: false
});

Параметр:

allowHTML: false

заставляет библиотеку интерпретировать значения как обычный текст.


Что происходит при отключённом HTML

Ввод:

<b>Admin</b>

будет отображён как:

&lt;b&gt;Admin&lt;/b&gt;

а не как жирный текст.


Почему allowHTML критически важен

При значении:

allowHTML: true

любая строка может стать частью DOM-структуры.

Особенно опасны:

  • пользовательские комментарии;
  • теги;
  • результаты поиска;
  • метки категорий;
  • данные из внешнего API.

Санитизация перед добавлением элементов

Даже при отключённом HTML полезно выполнять предварительную очистку.

Простая функция экранирования

function sanitize(text) {
  return text
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#039;');
}

Использование перед setChoices

const safeData = data.map(item => ({
  value: sanitize(item.value),
  label: sanitize(item.label)
}));

choices.setChoices(safeData, 'value', 'label', true);

Защита пользовательских тегов

new Choices(element, {
  addItems: true,
  addChoices: true,
  allowHTML: false
});

Дополнительная проверка:

element.addEventListener('addItem', event => {
  event.detail.value = sanitize(event.detail.value);
});

Использование DOMPurify

Для полноценной очистки HTML обычно применяется библиотека DOMPurify.

Подключение

<script src="https://cdn.jsdelivr.net/npm/dompurify@3.0.6/dist/purify.min.js"></script>

Очистка данных

const clean = DOMPurify.sanitize(userInput);

Использование с Choices.js

const safeChoices = apiData.map(item => ({
  value: DOMPurify.sanitize(item.value),
  label: DOMPurify.sanitize(item.label)
}));

choices.setChoices(safeChoices, 'value', 'label', true);

Запрет опасных тегов

DOMPurify.sanitize(input, {
  FORBID_TAGS: ['script', 'style']
});

Запрет опасных атрибутов

DOMPurify.sanitize(input, {
  FORBID_ATTR: ['onclick', 'onerror']
});

Санитизация в callbackOnCreateTemplates

Потенциально опасный код

callbackOnCreateTemplates: function(template) {
  return {
    item: data => {
      return template(`
        <div>${data.label}</div>
      `);
    }
  };
}

Если data.label содержит HTML, он попадёт в DOM.


Безопасный вариант

callbackOnCreateTemplates: function(template) {
  return {
    item: data => {
      const safeLabel = DOMPurify.sanitize(data.label);

      return template(`
        <div>${safeLabel}</div>
      `);
    }
  };
}

Полное исключение innerHTML

Наиболее безопасный подход — создание DOM-элементов вручную.

callbackOnCreateTemplates: function() {
  return {
    item: data => {
      const div = document.createElement('div');
      div.textContent = data.label;

      return div;
    }
  };
}

textContent автоматически экранирует HTML.


Проверка URL

Опасные протоколы

Следующие схемы должны блокироваться:

jav * ascript:
dat a:
vb * script:

Проверка ссылок

function isSafeUrl(url) {
  return /^(https?:\/\/)/i.test(url);
}

Использование

if (isSafeUrl(userLink)) {
  choices.setChoices([
    {
      value: userLink,
      label: userLink
    }
  ]);
}

Ограничение длины ввода

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

  • для переполнения интерфейса;
  • замедления поиска;
  • DoS-атак;
  • нарушения рендеринга.

Ограничение через addItemFilter

new Choices(element, {
  addItemFilter: value => value.length <= 50
});

Ограничение регулярным выражением

new Choices(element, {
  addItemFilter: /^[a-zA-Z0-9-_ ]{1,50}$/
});

Фильтрация специальных символов

Удаление HTML-тегов

function stripTags(text) {
  return text.replace(/<[^>]*>?/gm, '');
}

Удаление JavaScript-событий

function removeEvents(text) {
  return text.replace(/on\w+="[^"]*"/g, '');
}

Нормализация пробелов

function normalizeSpaces(text) {
  return text.replace(/\s+/g, ' ').trim();
}

Серверная санитизация

Клиентская защита не считается надёжной без проверки на сервере.

Пользователь может:

  • отключить JavaScript;
  • изменить запрос;
  • отправить данные вручную;
  • использовать DevTools;
  • вызывать API напрямую.

Проверка на Node.js

import validator from 'validator';

const safe = validator.escape(input);

Express middleware

app.use((req, res, next) => {
  if (req.body.name) {
    req.body.name = validator.escape(req.body.name);
  }

  next();
});

Проверка whitelist

const allowed = ['admin', 'user', 'moderator'];

if (!allowed.includes(role)) {
  throw new Error('Invalid role');
}

Санитизация данных API

Опасный пример

{
  "label": "<img src=x oner ror=alert(1)>"
}

Безопасная обработка

fetch('/api/options')
  .then(response => response.json())
  .then(data => {
    const safe = data.map(item => ({
      value: DOMPurify.sanitize(item.value),
      label: DOMPurify.sanitize(item.label)
    }));

    choices.setChoices(safe, 'value', 'label', true);
  });

Использование textContent вместо innerHTML

Опасный код

element.innerHTML = userData;

Безопасный код

element.textContent = userData;

Почему textContent безопаснее

textContent:

  • не интерпретирует HTML;
  • не запускает JavaScript;
  • автоматически экранирует спецсимволы;
  • защищает от большинства DOM XSS.

Content Security Policy

Даже при наличии санитизации рекомендуется использовать CSP.

Пример заголовка

Content-Security-Policy:
default-src 'self';
script-src 'self';
object-src 'none';

Что даёт CSP

Политика безопасности:

  • запрещает inline-скрипты;
  • ограничивает загрузку ресурсов;
  • блокирует часть XSS;
  • уменьшает последствия уязвимости.

Проверка пользовательских тегов

Контроль добавляемых значений

new Choices(element, {
  addItemFilter(value) {
    return /^[a-zA-Z0-9 ]+$/.test(value);
  }
});

Блокировка HTML

addItemFilter(value) {
  return !/<|>|script/i.test(value);
}

Безопасная работа с searchFields

Потенциальная проблема

Если поиск осуществляется по HTML-полю:

searchFields: ['label']

и label содержит вредоносный код, возможны проблемы рендеринга.


Безопасный подход

Использовать предварительно очищенные поля:

const safeData = data.map(item => ({
  value: item.value,
  label: stripTags(item.label)
}));

Двойная санитизация

Иногда данные проходят несколько этапов обработки:

  1. сервер;
  2. API;
  3. клиент;
  4. Choices.js;
  5. шаблонизатор.

Важно избегать двойного экранирования.


Проблемный результат

&amp;lt;script&amp;gt;

Причина

Данные были экранированы несколько раз.


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

Валидация строк

function validateString(value) {
  return typeof value === 'string';
}

Проверка массива

if (Array.isArray(data)) {
  // обработка
}

Проверка объекта

function isObject(value) {
  return value !== null && typeof value === 'object';
}

Защита от JSON-инъекций

Небезопасный подход

const data = JSON.parse(userInput);

Безопасная обработка

try {
  const data = JSON.parse(userInput);
} catch (error) {
  console.error('Invalid JSON');
}

Логирование подозрительного ввода

Пример

function detectAttack(value) {
  const patterns = [
    /<script/i,
    /onerror/i,
    /jav * ascript:/i
  ];

  return patterns.some(pattern => pattern.test(value));
}

Использование

if (detectAttack(input)) {
  console.warn('Possible XSS attempt');
}

Архитектура безопасной обработки

Надёжная схема работы с пользовательским вводом включает:

Этап Действие
Ввод Проверка формата
Клиент Санитизация
Choices.js allowHTML: false
API Валидация
Сервер Экранирование
База данных Хранение очищенных данных
DOM Использование textContent
Браузер CSP

Типичные ошибки

Использование allowHTML: true без фильтрации

allowHTML: true

без DOMPurify — распространённая причина XSS.


Прямое использование innerHTML

container.innerHTML = value;

Доверие данным API

Даже внутренний API может вернуть вредоносный контент.


Отсутствие серверной проверки

Клиентская защита обходится за секунды.


Санитизация только при отображении

Опасные данные уже могут быть сохранены в базе.


Рекомендованный безопасный шаблон

import DOMPurify from 'dompurify';

const choices = new Choices(element, {
  allowHTML: false,
  addItemFilter: value => {
    return /^[a-zA-Z0-9-_ ]{1,50}$/.test(value);
  }
});

fetch('/api/options')
  .then(response => response.json())
  .then(data => {
    const safeData = data.map(item => ({
      value: DOMPurify.sanitize(String(item.value)),
      label: DOMPurify.sanitize(String(item.label))
    }));

    choices.setChoices(safeData, 'value', 'label', true);
  });

Практические рекомендации

На клиенте

  • отключать HTML;
  • ограничивать длину;
  • проверять регулярными выражениями;
  • использовать DOMPurify;
  • избегать innerHTML.

На сервере

  • валидировать все поля;
  • использовать whitelist;
  • экранировать HTML;
  • фильтровать URL;
  • логировать подозрительную активность.

В интерфейсе

  • использовать textContent;
  • включать CSP;
  • не доверять API;
  • минимизировать пользовательский HTML;
  • изолировать шаблоны.