XSS предотвращение

MapLibre GL JS активно использует декларативную модель отображения данных (стили, источники, слои, всплывающие окна), где значительная часть информации может приходить из внешних или динамических источников. Именно это создаёт несколько характерных зон риска для XSS:

  • всплывающие окна (popups) и tooltip’ы с HTML-контентом
  • свойства GeoJSON и feature properties, используемые в стиле
  • динамически формируемые стили (style JSON)
  • пользовательские источники данных (vector tiles, GeoJSON endpoints)
  • кастомные DOM-элементы поверх карты

Ключевая особенность заключается в том, что MapLibre GL JS не является «веб-приложением с формами», но активно отображает внешние данные, которые часто интерпретируются как текст или HTML.


Popups как основная точка внедрения скриптов

Popups в MapLibre GL JS позволяют задавать HTML напрямую:

new maplibregl.Popup()
  .setHTML(`<b>${feature.properties.name}</b>`)
  .setLngLat([lng, lat])
  .addTo(map);

Опасность возникает при прямой вставке данных из feature.properties, особенно если они приходят из GeoJSON или API.

Уязвимый сценарий

map.on('click', 'places', (e) => {
  const feature = e.features[0];

  new maplibregl.Popup()
    .setHTML(feature.properties.description)
    .setLngLat(e.lngLat)
    .addTo(map);
});

Если description содержит:

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

происходит выполнение JavaScript в контексте страницы.


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

Базовый принцип — отказ от интерпретации HTML там, где это не требуется.

Использование textContent через DOM-узлы

const popupContent = document.createElement('div');
popupContent.textContent = feature.properties.description;

new maplibregl.Popup()
  .setDOMContent(popupContent)
  .setLngLat(e.lngLat)
  .addTo(map);

textContent гарантирует, что любые HTML-теги будут экранированы.


Ограничение HTML через whitelist-рендеринг

Если HTML необходим (например, для форматирования карточек объектов), применяется строгая фильтрация.

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

import DOMPurify from 'dompurify';

const cleanHTML = DOMPurify.sanitize(feature.properties.description);

new maplibregl.Popup()
  .setHTML(cleanHTML)
  .setLngLat(e.lngLat)
  .addTo(map);

Важно учитывать конфигурацию:

  • запрет onerror, onclick, onload
  • удаление script, iframe, object

XSS через GeoJSON properties

GeoJSON часто поступает из внешних API или пользовательских загрузок.

Потенциально опасная модель

map.addSource('points', {
  type: 'geojson',
  data: geojsonData
});

Далее свойства используются в стилях или popups:

map.setLayoutProperty('label', 'text-field', ['get', 'name']);

Сам по себе text-field безопасен, но риск возникает при:

  • подстановке в HTML popups
  • использовании feature-state + DOM-рендеринга
  • генерации custom markers

HTML-маркеры и DOM-based XSS

MapLibre позволяет использовать кастомные DOM-элементы:

const el = document.createElement('div');
el.innerHTML = feature.properties.iconHtml;

new maplibregl.Marker(el)
  .setLngLat([lng, lat])
  .addTo(map);

Это прямой XSS-вектор.

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

const el = document.createElement('div');
el.textContent = feature.properties.label;

new maplibregl.Marker(el)
  .setLngLat([lng, lat])
  .addTo(map);

Уязвимости через style JSON

Стиль MapLibre — это JSON, который может загружаться удалённо.

Опасность возникает, если:

  • стиль генерируется пользователями
  • style URL не контролируется
  • используются нестабильные источники

Рискованный сценарий

map.setStyle(userProvidedStyleUrl);

Если URL контролируется атакующим, он может:

  • изменить источники данных
  • внедрить неожиданные выражения
  • подменить изображения/иконки

Защита

  • белый список style URL
  • локальная валидация JSON перед применением
  • запрет удалённых неизвестных источников

Expressions и косвенные инъекции

MapLibre GL JS поддерживает expression-синтаксис:

['concat', ['get', 'name'], ' ', ['get', 'type']]

Сам по себе он не исполняет JS, но становится опасным при:

  • генерации выражений из пользовательского ввода
  • динамической сборке массива выражений

Уязвимый пример

const expr = ['concat', userInput];

map.setPaintProperty('layer', 'text-color', expr);

Если userInput не контролируется, возможно:

  • логическое повреждение выражения
  • обход бизнес-логики
  • неожиданные DOM-эффекты через style cascade

Защита через типизацию и схемы данных

При работе с внешними данными важно вводить строгие контракты:

  • проверка типов (string/number/boolean)
  • ограничение длины строк
  • запрет HTML-символов (<, >, &) в текстовых полях
  • валидация координат и feature properties

Пример валидации:

function sanitizeFeature(f) {
  return {
    ...f,
    properties: {
      name: String(f.properties.name ?? '').slice(0, 100),
      description: String(f.properties.description ?? '').slice(0, 300)
    }
  };
}

Изоляция данных и принцип доверенных границ

Архитектурный подход заключается в разделении:

  • недоверенные данные (API, пользовательские GeoJSON)
  • доверенные данные (локальные источники, backend)
  • слой рендеринга (MapLibre)

Основное правило — MapLibre не должен получать «сырые HTML-данные».


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

Sprites и icon-image могут быть подменены через внешние URL.

Риск:

  • загрузка вредоносного SVG
  • подмена ресурсов через CDN

Защита

  • фиксированный sprite URL
  • запрет user-defined image URLs
  • использование локального ассета-бандла

Content Security Policy в контексте MapLibre

CSP значительно снижает риск XSS даже при ошибках в коде.

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

  • запрет unsafe-inline
  • ограничение script-src только на доверенные домены
  • ограничение img-src и worker-src

Особенно важно:

  • MapLibre использует Web Workers
  • источники данных могут загружаться через fetch/XHR

Событийная модель и DOM-инъекции

Обработчики событий (click, mousemove) часто становятся местом сборки UI:

map.on('click', layerId, (e) => {
  document.getElementById('panel').innerHTML =
    e.features[0].properties.name;
});

Проблема возникает при использовании innerHTML.

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

document.getElementById('panel').textContent =
  e.features[0].properties.name;

Итоговая модель защиты

Безопасное использование MapLibre GL JS строится на сочетании:

  • отказ от innerHTML в пользу textContent
  • строгая санитизация HTML при необходимости
  • контроль источников стилей и данных
  • валидация GeoJSON и feature properties
  • ограничение expression generation
  • CSP как слой защиты на уровне браузера

Такая модель снижает риск XSS до уровня, при котором MapLibre выступает только как рендеринг-движок, не участвующий в интерпретации пользовательского HTML-кода.