XSS-уязвимости

В приложениях, использующих интерактивные карты, значительная часть данных поступает из внешних источников: API геоданных, пользовательский ввод, GeoJSON-файлы, сторонние сервисы. Именно эта особенность делает подобные приложения чувствительными к XSS-уязвимостям (Cross-Site Scripting), особенно при использовании библиотек визуализации вроде Leaflet.

Leaflet активно работает с HTML-контентом внутри попапов, тултипов и кастомных слоёв. Это создаёт поверхность атаки, если данные вставляются без строгой фильтрации и экранирования.


Механика XSS в контексте Leaflet

XSS возникает, когда вредоносный JavaScript попадает в DOM и выполняется в контексте страницы. В Leaflet это чаще всего происходит через:

  • вставку HTML в bindPopup и bindTooltip
  • использование пользовательских свойств GeoJSON
  • динамическое создание содержимого маркеров
  • генерацию HTML через шаблонизаторы без экранирования
  • вставку внешних данных через innerHTML

Leaflet не выполняет автоматическую санитизацию контента. Библиотека предполагает, что разработчик контролирует источник данных.


Попапы и тултипы как основной вектор атак

Наиболее частая точка возникновения XSS — метод bindPopup.

marker.bindPopup(userInput);

Если userInput содержит HTML:

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

код будет вставлен в DOM и выполнен.

Leaflet позволяет передавать HTML-строки напрямую, что делает любые неподготовленные данные потенциально опасными.

Более сложный вариант:

marker.bindPopup(`<b>${title}</b><br>${description}`);

Если title или description приходят извне, внедрение <script> или обработчиков событий становится возможным.


GeoJSON и скрытая инъекция

GeoJSON — один из наиболее частых источников данных в Leaflet через L.geoJSON.

L.geoJSON(data, {
  onEachFeature: function (feature, layer) {
    layer.bindPopup(feature.properties.description);
  }
}).addTo(map);

Если feature.properties.description контролируется пользователем или внешним API, внедрение HTML превращается в прямой XSS-вектор.

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

  • properties.name
  • properties.description
  • properties.html
  • любые кастомные атрибуты, используемые в шаблонах

Даже если GeoJSON приходит с доверенного сервера, компрометация источника данных автоматически переносится в клиент.


Опасность innerHTML при кастомных слоях

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

const div = L.DomUtil.create('div', 'custom-marker');
div.innerHTML = userContent;

Любая вставка innerHTML без очистки превращает слой в потенциальный XSS-контейнер.

Даже более «безобидные» конструкции:

marker.bindPopup(`<div class="popup">${content}</div>`);

становятся уязвимыми, если content не экранирован.


SVG и HTML-оверлеи

Leaflet поддерживает кастомные оверлеи, включая SVG-слои и HTML-слои через L.divIcon.

L.divIcon({
  html: userHtml
});

или

L.svg().addTo(map);

SVG особенно опасен, поскольку поддерживает встроенные события:

<svg onl oad="alert(1)">

Если SVG генерируется из внешних данных без очистки, XSS становится прямым.


Сценарии с шаблонизацией

Часто Leaflet используется вместе с шаблонизаторами (Handlebars, Mustache, EJS).

Ошибки возникают при отключённом экранировании:

const html = `<div>${template(data)}</div>`;
marker.bindPopup(html);

Если шаблон вставляет значения без HTML-escape, любой пользовательский ввод становится исполняемым кодом.

Особенно опасны конструкции вида:

  • {{{raw}}} (Handlebars)
  • unescaped output в EJS
  • ручная конкатенация строк

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

Основной защитный механизм — очистка HTML перед вставкой в Leaflet.

Используются специализированные библиотеки:

  • DOMPurify
  • sanitize-html

Пример с DOMPurify:

const clean = DOMPurify.sanitize(userInput);
marker.bindPopup(clean);

DOMPurify удаляет:

  • script-теги
  • inline-обработчики событий
  • опасные URI (jav * ascript:)
  • SVG-эксплойты

Разделение данных и представления

Критически важный принцип — хранение данных отдельно от HTML.

Плохой подход:

feature.properties.popup = "<b>" + name + "</b>";

Корректный подход:

feature.properties.name = name;

и формирование HTML на клиенте:

layer.bindPopup(`
  <b>${escapeHtml(feature.properties.name)}</b>
`);

где escapeHtml экранирует спецсимволы:

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

Уязвимости через события DOM

Leaflet часто используется с обработчиками событий:

marker.on('click', function (e) {
  popup.setContent(userContent);
});

Если userContent формируется динамически, XSS может возникать не сразу, а через цепочку событий.

Особенно опасны конструкции:

  • обновление popups по AJAX
  • реактивные данные (React/Vue интеграции)
  • WebSocket-обновления геоданных

CSP как дополнительный слой защиты

Content Security Policy ограничивает выполнение скриптов даже при наличии инъекции.

Рекомендуемые ограничения:

  • запрет unsafe-inline
  • запрет unsafe-eval
  • строгие script-src домены

Пример политики:

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

CSP не заменяет санитизацию, но снижает ущерб.


Leaflet-плагины как скрытый риск

Экосистема Leaflet включает множество сторонних плагинов:

  • кластеризация маркеров
  • кастомные popups
  • heatmap-слои
  • редакторы геометрии

Некоторые плагины используют innerHTML без очистки или предполагают доверенные данные.

Типовая проблема:

L.popupPlugin.setContent(data.html);

если data.html поступает извне — возникает XSS вне основного кода приложения.


Маскирование атак через GeoJSON свойства

Атаки часто выглядят «невинно» в структуре GeoJSON:

{
  "type": "Feature",
  "properties": {
    "description": "<img src=x oner ror=alert(1)>"
  }
}

При визуализации через popup это превращается в выполнение кода.

Даже URL-поля могут быть опасны:

"website": "jav * ascript:alert(1)"

Защита на уровне архитектуры

Системный подход включает несколько уровней:

Контроль источников данных

  • доверенные API
  • подпись данных (JWT, HMAC)
  • валидация GeoJSON схем

Экранирование на клиенте

  • ручной escape HTML
  • запрет innerHTML для внешних данных

Санитизация HTML

  • DOMPurify перед вставкой в Leaflet

Ограничение API Leaflet

  • использование текстовых popups вместо HTML
  • отказ от html: в L.divIcon при внешних данных

Безопасные паттерны работы с popups

Строгая модель:

layer.bindPopup(document.createTextNode(name));

или:

layer.bindPopup(escapeHtml(name));

или использование безопасного DOM API:

const div = document.createElement('div');
div.textContent = userInput;
marker.bindPopup(div);

textContent полностью исключает интерпретацию HTML.


Интеграция с современными фронтенд-фреймворками

При использовании React/Vue/Svelte часто возникает конфликт моделей:

  • Leaflet ожидает HTML строки
  • фреймворк управляет DOM

Опасный анти-паттерн:

marker.bindPopup(renderToString(component));

если компонент содержит непроверенные props.

Безопаснее:

  • рендерить только текстовые значения
  • избегать dangerouslySetInnerHTML внутри map callbacks

Работа с пользовательскими слоями и редакторами

Редакторы геометрии (draw tools) позволяют пользователю создавать объекты, которые затем сохраняются и отображаются.

Если такие объекты содержат HTML-поля, XSS становится постоянным (stored XSS):

  1. пользователь сохраняет объект с payload
  2. объект попадает в базу
  3. отображается всем пользователям через Leaflet

Особенно критично для:

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

Практические защитные стратегии

  • отказ от HTML в данных GeoJSON
  • использование textContent вместо innerHTML
  • обязательная санитизация всех popup-данных
  • запрет inline-event handlers (onclick, onload)
  • фильтрация SVG перед отображением
  • строгая схема данных для GeoJSON
  • разделение доверенных и недоверенных слоёв
  • CSP с запретом inline script
  • регулярный аудит Leaflet-плагинов
  • изоляция сторонних источников данных на сервере до попадания в клиентскую карту