Автодополнение на основе пользовательского ввода относится к классу интерфейсов, которые постоянно обрабатывают недоверенные данные. В контексте Awesomplete это особенно критично, поскольку библиотека часто используется для отображения динамически формируемых списков подсказок, которые могут поступать как из локальных массивов, так и с удалённых API.
Основная проблема заключается в том, что любой элемент списка подсказок фактически становится частью DOM и потенциально может быть интерпретирован браузером как HTML-код. Это создаёт поверхность атаки для XSS, внедрения HTML-инъекций и подмены содержимого интерфейса.
Ключевой принцип: любые данные, пришедшие извне, должны рассматриваться как небезопасные по умолчанию
Awesomplete по умолчанию работает с текстовыми значениями, однако при неправильной интеграции разработчики часто начинают передавать в список строковые значения, содержащие HTML-разметку.
Типичный рискованный сценарий:
new Awesomplete(input, {
list: ["<b>JavaScript</b>", "<i>Python</i>"]
});
Если библиотека или кастомная реализация рендеринга использует
innerHTML, подобные значения могут быть интерпретированы
как DOM-структура, а не как текст. Это открывает путь к выполнению
произвольного HTML/JS.
Любое отображение пользовательских или внешних данных в DOM должно происходить через текстовые механизмы браузера.
Безопасный подход:
const item = document.createElement("li");
item.textContent = value;
Небезопасный подход:
item.innerHTML = value;
В контексте Awesomplete это особенно важно при кастомизации
отображения элементов через item callback.
Awesomplete позволяет передавать список подсказок напрямую или через асинхронные источники. В обоих случаях необходимо гарантировать, что данные проходят этап нормализации.
function sanitizeList(arr) {
return arr
.filter(item => typeof item === "string")
.map(item => item
.replace(/</g, "<")
.replace(/>/g, ">")
);
}
Использование:
new Awesomplete(input, {
list: sanitizeList(rawData)
});
Однако более надёжный подход — не хранить HTML-сущности в данных
вообще, а всегда работать с чистыми строками и использовать
textContent на этапе рендера.
Awesomplete позволяет переопределять поведение отображения через
опцию item. Именно здесь чаще всего возникает
уязвимость.
item: function(text, input) {
const li = document.createElement("li");
li.innerHTML = text;
return li;
}
item: function(text) {
const li = document.createElement("li");
li.textContent = text;
return li;
}
Если требуется подсветка совпадений, нельзя напрямую вставлять HTML. Вместо этого используется разбиение строки и сборка DOM-узлов:
item: function(text, input) {
const li = document.createElement("li");
const index = text.toLowerCase().indexOf(input.toLowerCase());
if (index >= 0) {
const before = text.slice(0, index);
const match = text.slice(index, index + input.length);
const after = text.slice(index + input.length);
li.appendChild(document.createTextNode(before));
const mark = document.createElement("span");
mark.textContent = match;
mark.style.fontWeight = "bold";
li.appendChild(mark);
li.appendChild(document.createTextNode(after));
} else {
li.textContent = text;
}
return li;
}
При использовании API для подгрузки подсказок появляется дополнительный слой риска: данные могут быть скомпрометированы на сервере или в промежуточных слоях.
fetch("/api/suggestions?q=" + encodeURIComponent(query))
.then(res => res.json())
.then(data => {
const safe = data
.filter(x => typeof x.name === "string")
.map(x => x.name);
new Awesomplete(input, { list: safe });
});
Опасность возникает не только в HTML, но и в структуре данных. Например, если API возвращает неожиданные поля или вложенные объекты, их нельзя напрямую передавать в Awesomplete.
Небезопасно:
list: data.items
Без проверки структуры.
Безопасно:
function extractSuggestions(data) {
if (!data || !Array.isArray(data.items)) return [];
return data.items
.map(x => x && x.label)
.filter(x => typeof x === "string");
}
Awesomplete предоставляет filter и replace
callbacks. Эти функции часто используются для кастомизации поведения, но
могут стать точкой внедрения логики, нарушающей безопасность.
filter: function(text, input) {
return text.toLowerCase().includes(input.toLowerCase());
}
Запрещено использовать eval-подобные конструкции или динамическое выполнение кода внутри фильтрации.
При работе с текстом важно учитывать HTML-специфичные символы:
<>&"Универсальная функция экранирования:
function escapeHtml(str) {
return str
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
Однако при использовании textContent необходимость в
таком экранировании исчезает, что делает его предпочтительным способом
отображения.
Любая строка, попадающая в DOM, должна проходить через один из двух безопасных путей:
textContent (предпочтительно)Запрещённые практики:
innerHTML с внешними даннымиДаже при наличии ошибок в коде можно существенно снизить риск эксплуатации через CSP.
Рекомендуемые директивы:
unsafe-inlinescript-src только доверенными
источникамиПример концептуальной политики:
Content-Security-Policy: script-src 'self'; object-src 'none'; base-uri 'self';
Ввод пользователя сам по себе не должен попадать в логику формирования списка без обработки.
Типовой безопасный pipeline:
Пример:
input.addEventListener("input", () => {
const q = input.value.trim();
if (q.length < 2) return;
fetch("/api?q=" + encodeURIComponent(q))
.then(r => r.json())
.then(data => {
const list = extractSuggestions(data);
awesomplete.list = list;
});
});
Сильная архитектурная ошибка — смешивание UI-логики и данных. В случае Awesomplete это выражается в передаче “готового HTML” вместо структурированных данных.
Правильная модель:
itemТакое разделение минимизирует вероятность внедрения вредоносных данных в интерфейс.
В реальных условиях данные могут содержать:
nullundefinedУстойчивый фильтр:
function normalize(value) {
if (typeof value !== "string") return "";
return value.trim();
}
И последующая очистка:
const safeList = raw.map(normalize).filter(Boolean);
При работе с динамическими источниками полезно вводить дополнительную проверку уникальности и допустимых значений.
function unique(arr) {
return [...new Set(arr)];
}
Это снижает риск дублирования и косвенно уменьшает поверхность атак, связанных с повторной вставкой вредоносных значений.
Безопасная архитектура использования библиотеки строится вокруг трёх принципов:
textContent вместо
innerHTMLЛюбое отклонение от этих принципов превращает автодополнение в точку входа для DOM-инъекций и XSS-атак, особенно в сценариях с удалёнными API и пользовательским контентом.