XSS защита

В приложениях, использующих Mapbox и библиотеку Mapbox GL JS, уязвимости класса XSS (Cross-Site Scripting) возникают не в самом WebGL-рендерере, а на границе взаимодействия карты с пользовательскими данными и DOM.

Основной риск формируется там, где геоданные или свойства объектов попадают в HTML-контекст без экранирования: всплывающие окна, маркеры, атрибуты слоёв и кастомные UI-компоненты.


Источники потенциально опасных данных

GeoJSON и свойства feature

Наиболее частый источник XSS — свойства объектов GeoJSON:

{
  "type": "Feature",
  "geometry": {
    "type": "Point",
    "coordinates": [30.5, 50.5]
  },
  "properties": {
    "title": "<img src=x oner ror=alert(1)>",
    "description": "<script>alert('xss')</script>"
  }
}

При отображении этих полей в попапах или UI без фильтрации возникает выполнение произвольного JavaScript.


Всплывающие окна (Popup)

Mapbox GL JS предоставляет API для всплывающих окон:

new mapboxgl.Popup()
  .setLngLat([30.5, 50.5])
  .setHTML(feature.properties.description)
  .addTo(map);

Метод setHTML() является прямой точкой риска, поскольку вставляет строку как HTML без автоматической санации.

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

new mapboxgl.Popup()
  .setLngLat([30.5, 50.5])
  .setText(feature.properties.description)
  .addTo(map);

setText() использует textContent, исключая интерпретацию HTML.


DOM-маркеры и кастомные элементы

Создание маркеров через DOM:

const el = document.createElement('div');
el.innerHTML = feature.properties.iconHtml;
new mapboxgl.Marker(el).setLngLat([30, 50]).addTo(map);

Опасность возникает при использовании innerHTML с данными, пришедшими извне.

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

const el = document.createElement('div');
el.textContent = feature.properties.label;
new mapboxgl.Marker(el).setLngLat([30, 50]).addTo(map);

Уязвимые точки в архитектуре Mapbox GL JS

1. HTML-рендеринг пользовательских данных

Любая функция, связанная с:

  • setHTML
  • innerHTML
  • шаблонами строк
  • динамической генерацией DOM

является потенциальным вектором XSS.


2. Expressions и style properties

Стили Mapbox используют выражения:

"paint": {
  "text-field": ["get", "name"]
}

Сам по себе text-field не выполняет HTML, однако при использовании format-выражений и кастомных UI-слоёв риск появляется при переносе значения в DOM.


3. Popups и HTML-шаблоны

Типичный анти-паттерн:

const html = `
  <h3>${feature.properties.title}</h3>
  <p>${feature.properties.description}</p>
`;

popup.setHTML(html);

Если title или description управляются пользователем, это прямой XSS.


Принципы защиты от XSS

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

DOM API:

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

Ключевой принцип: никакого HTML из недоверенных источников.


Экранирование данных перед вставкой в HTML

Если HTML необходим, данные должны быть экранированы:

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

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

popup.setHTML(`
  <h3>${escapeHtml(feature.properties.title)}</h3>
`);

Полное избегание setHTML для пользовательских данных

setHTML() допустим только при работе с:

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

CSP (Content Security Policy)

Дополнительный слой защиты:

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

Это ограничивает выполнение inline-scripts даже при ошибках в коде.


Работа с GeoJSON как источником данных

Санитизация на этапе загрузки

Перед добавлением данных в источник карты:

function sanitizeFeature(feature) {
  return {
    ...feature,
    properties: {
      title: String(feature.properties.title),
      description: String(feature.properties.description)
    }
  };
}

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

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

const blacklist = /<script|onerror|onload|jav * ascript:/i;

function isSafe(value) {
  return !blacklist.test(value);
}

Безопасная архитектура всплывающих окон

Рекомендуемая модель:

map.on('click', 'points', (e) => {
  const props = e.features[0].properties;

  const el = document.createElement('div');

  const title = document.createElement('h3');
  title.textContent = props.title;

  const desc = document.createElement('p');
  desc.textContent = props.description;

  el.appendChild(title);
  el.appendChild(desc);

  new mapboxgl.Popup()
    .setLngLat(e.lngLat)
    .setDOMContent(el)
    .addTo(map);
});

Ключевая идея: setDOMContent вместо setHTML.


Маркеры как вектор XSS-риска

Маркер с пользовательским HTML:

const el = document.createElement('div');
el.innerHTML = userData.badge;

Безопасная замена:

const el = document.createElement('div');
el.className = 'marker';
el.textContent = userData.label;

Работа с изображениями

Даже изображения могут быть вектором атаки:

const img = document.createElement('img');
img.src = feature.properties.imageUrl;

Риски:

  • jav * ascript: URI
  • data URL с вредоносным содержимым
  • подмена ресурсов

Защита:

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

Взаимодействие с событиями карты

map.on('click', (e) => {
  console.log(e.features);
});

Опасность возникает при передаче e.features напрямую в DOM без обработки.


Data-driven styling и безопасность

Даже стили могут содержать пользовательские строки:

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

Сам рендер безопасен, но риск появляется при выводе значений в HTML-интерфейс.


Типичные ошибки интеграции

  • использование innerHTML для данных из GeoJSON
  • применение setHTML для описаний объектов
  • вставка URL без проверки протокола
  • доверие данным из сторонних API
  • отсутствие CSP
  • отсутствие централизованной санитизации

Модель доверия к данным

Данные в Mapbox-приложениях следует разделять:

  • доверенные: статические стили, серверный контент
  • частично доверенные: API с контролем
  • недоверенные: пользовательские GeoJSON, внешние источники, crowd-sourced данные

Каждый слой должен обрабатываться отдельно перед попаданием в DOM.


Изоляция UI-слоя от данных карты

Архитектурный принцип:

  • карта отвечает за визуализацию
  • UI отвечает за отображение текста
  • данные проходят через слой нормализации
GeoJSON → sanitizer → UI model → DOM render

Итоговая модель безопасного рендеринга

Безопасная работа с Mapbox GL JS строится вокруг трёх принципов:

  • исключение HTML из пользовательских данных
  • строгая фильтрация GeoJSON перед использованием
  • использование DOM API вместо HTML-инъекций