Санитизация пользовательского ввода — процесс очистки, нормализации и фильтрации данных, поступающих от пользователя, перед их отображением, сохранением или передачей в приложение. В контексте работы с библиотекой Choices.js эта тема особенно важна, поскольку библиотека активно взаимодействует с текстовыми значениями, HTML-разметкой, пользовательскими тегами, элементами поиска и динамически создаваемыми DOM-узлами.
Без корректной санитизации приложение становится уязвимым для:
Особенно критична защита в следующих сценариях:
Библиотека получает данные из нескольких источников:
<input type="text">
или:
<select>
<option>...</option>
</select>
choices.setChoices(data, 'value', 'label', true);
new Choices(element, {
addItems: true,
addChoices: true
});
callbackOnCreateTemplates
fetch('/api/tags')
Опасный ввод:
<script>alert('XSS')</script>
или:
<img src=x oner ror=alert(1)>
Если такие значения будут вставлены через innerHTML,
произойдёт выполнение вредоносного кода.
Уязвимость возникает при прямой вставке пользовательских данных в DOM:
element.innerHTML = userInput;
Опасное значение:
" oncl ick="alert(1)
может нарушить структуру HTML.
Пример:
jav * ascript:alert(1)
Библиотека содержит встроенные механизмы экранирования, однако их недостаточно для полной защиты приложения.
Choices.js:
Поэтому безопасность должна обеспечиваться на нескольких уровнях:
Наиболее безопасный подход — полный отказ от HTML в пользовательских данных.
const choices = new Choices(element, {
allowHTML: false
});
Параметр:
allowHTML: false
заставляет библиотеку интерпретировать значения как обычный текст.
Ввод:
<b>Admin</b>
будет отображён как:
<b>Admin</b>
а не как жирный текст.
При значении:
allowHTML: true
любая строка может стать частью DOM-структуры.
Особенно опасны:
Даже при отключённом HTML полезно выполнять предварительную очистку.
function sanitize(text) {
return text
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
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);
});
Для полноценной очистки HTML обычно применяется библиотека DOMPurify.
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.0.6/dist/purify.min.js"></script>
const clean = DOMPurify.sanitize(userInput);
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: 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>
`);
}
};
}
Наиболее безопасный подход — создание DOM-элементов вручную.
callbackOnCreateTemplates: function() {
return {
item: data => {
const div = document.createElement('div');
div.textContent = data.label;
return div;
}
};
}
textContent автоматически экранирует HTML.
Следующие схемы должны блокироваться:
jav * ascript:
dat a:
vb * script:
function isSafeUrl(url) {
return /^(https?:\/\/)/i.test(url);
}
if (isSafeUrl(userLink)) {
choices.setChoices([
{
value: userLink,
label: userLink
}
]);
}
Очень длинные строки могут использоваться:
new Choices(element, {
addItemFilter: value => value.length <= 50
});
new Choices(element, {
addItemFilter: /^[a-zA-Z0-9-_ ]{1,50}$/
});
function stripTags(text) {
return text.replace(/<[^>]*>?/gm, '');
}
function removeEvents(text) {
return text.replace(/on\w+="[^"]*"/g, '');
}
function normalizeSpaces(text) {
return text.replace(/\s+/g, ' ').trim();
}
Клиентская защита не считается надёжной без проверки на сервере.
Пользователь может:
import validator from 'validator';
const safe = validator.escape(input);
app.use((req, res, next) => {
if (req.body.name) {
req.body.name = validator.escape(req.body.name);
}
next();
});
const allowed = ['admin', 'user', 'moderator'];
if (!allowed.includes(role)) {
throw new Error('Invalid role');
}
{
"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);
});
element.innerHTML = userData;
element.textContent = userData;
textContent:
Даже при наличии санитизации рекомендуется использовать CSP.
Content-Security-Policy:
default-src 'self';
script-src 'self';
object-src 'none';
Политика безопасности:
new Choices(element, {
addItemFilter(value) {
return /^[a-zA-Z0-9 ]+$/.test(value);
}
});
addItemFilter(value) {
return !/<|>|script/i.test(value);
}
Если поиск осуществляется по HTML-полю:
searchFields: ['label']
и label содержит вредоносный код, возможны проблемы
рендеринга.
Использовать предварительно очищенные поля:
const safeData = data.map(item => ({
value: item.value,
label: stripTags(item.label)
}));
Иногда данные проходят несколько этапов обработки:
Важно избегать двойного экранирования.
&lt;script&gt;
Данные были экранированы несколько раз.
function validateString(value) {
return typeof value === 'string';
}
if (Array.isArray(data)) {
// обработка
}
function isObject(value) {
return value !== null && typeof value === 'object';
}
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
без DOMPurify — распространённая причина XSS.
container.innerHTML = value;
Даже внутренний 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);
});