Санитизация пользовательского ввода

В приложениях на Mapbox GL JS значительная часть логики строится вокруг пользовательского ввода: координаты поиска, фильтры объектов, параметры стилей, GeoJSON-данные, HTML-контент всплывающих окон и параметры URL. Любая точка, где данные приходят извне и попадают в рендеринг карты или управление состоянием, становится потенциальным вектором атаки.

Особенность Mapbox GL JS заключается в том, что он сочетает в себе три чувствительных слоя:

  • отрисовка WebGL-сцены
  • динамическая интерпретация стилей (style specification)
  • работа с внешними данными (GeoJSON, API, пользовательские запросы)

Каждый из этих слоёв требует строгого контроля входящих данных, поскольку некорректная или специально сформированная структура может привести к XSS, инъекциям выражений, перегрузке рендера или утечке данных.


Основные источники пользовательского ввода

В реальных приложениях данные, требующие санитизации, поступают из нескольких каналов:

  • параметры URL (?lng=, ?lat=, ?filter=)
  • поисковые строки (геокодинг, фильтрация слоёв)
  • пользовательские GeoJSON-объекты
  • формы добавления маркеров и слоёв
  • конфигурации стилей, полученные с сервера или клиента
  • HTML-содержимое popup и tooltip
  • данные из сторонних API

Каждый из этих источников должен рассматриваться как недоверенный, даже если он формируется внутри собственного фронтенда.


Координаты и числовая валидация

Классическая ошибка — прямое использование значений из query string или формы без проверки диапазонов:

const lng = parseFloat(params.get('lng'));
const lat = parseFloat(params.get('lat'));

map.setCenter([lng, lat]);

Без проверки возможно:

  • NaN-значения
  • выход за пределы допустимых координат
  • переполнение логики камеры

Корректный подход включает строгую проверку:

function isValidLat(lat) {
  return Number.isFinite(lat) && lat >= -90 && lat <= 90;
}

function isValidLng(lng) {
  return Number.isFinite(lng) && lng >= -180 && lng <= 180;
}

if (isValidLat(lat) && isValidLng(lng)) {
  map.setCenter([lng, lat]);
}

Дополнительно полезно ограничивать зум и bounds:

map.setMaxBounds([
  [-180, -85],
  [180, 85]
]);

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

GeoJSON часто поступает из пользовательских форм или сторонних источников. Опасность заключается не только в структуре данных, но и в свойствах properties, которые могут попасть в UI.

Типичный небезопасный сценарий:

map.addSource('user-data', {
  type: 'geojson',
  data: userInputGeojson
});

Проблемные места:

  • слишком большие геометрии (DoS через сложные полигоны)
  • лишние поля в properties
  • неожиданные типы координат
  • внедрение HTML в свойства

Минимальная проверка структуры:

function validateFeature(feature) {
  return (
    feature &&
    feature.type === 'Feature' &&
    feature.geometry &&
    Array.isArray(feature.geometry.coordinates)
  );
}

Для коллекций:

function sanitizeGeoJSON(data) {
  if (data.type !== 'FeatureCollection') return null;

  return {
    type: 'FeatureCollection',
    features: data.features
      .filter(validateFeature)
      .slice(0, 1000)
      .map(f => ({
        type: 'Feature',
        geometry: f.geometry,
        properties: {}
      }))
  };
}

Политика «обнуления properties» часто используется как наиболее безопасная стратегия при работе с пользовательскими данными.


HTML-инъекции в Popup и Tooltip

Mapbox GL JS позволяет создавать всплывающие окна с HTML-контентом:

new mapboxgl.Popup()
  .setHTML(userInput)
  .addTo(map);

Это прямой путь к XSS.

Любой пользовательский текст должен проходить экранирование:

function escapeHTML(str) {
  return str
    .replaceAll('&', '&amp;')
    .replaceAll('<', '&lt;')
    .replaceAll('>', '&gt;')
    .replaceAll('"', '&quot;')
    .replaceAll("'", '&#039;');
}

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

popup.setHTML(`<div>${escapeHTML(title)}</div>`);

Для более строгого подхода применяется политика «только текст»:

popup.setText(userInput);

Этот вариант полностью исключает HTML-инъекции и предпочтителен в большинстве случаев.


Защита от инъекций в фильтрах и выражениях

Mapbox GL JS использует expression syntax для фильтрации и стилей. Неправильная подстановка пользовательских данных может привести к логическим инъекциям или утечке данных через фильтры.

Опасный пример:

map.setFilter('points', ['==', ['get', 'type'], userInput]);

Если userInput невалиден, возможны неожиданные поведения фильтра.

Безопасный подход — whitelist:

const allowedTypes = new Set(['park', 'school', 'hospital']);

if (allowedTypes.has(userInput)) {
  map.setFilter('points', ['==', ['get', 'type'], userInput]);
}

URL-параметры и состояние карты

Часто состояние карты синхронизируется с URL:

const params = new URLSearchParams(window.location.search);

Проблемы возникают при отсутствии нормализации:

  • строковые значения вместо чисел
  • перепутанные координаты
  • попытки внедрения скриптов в параметры состояния

Пример безопасной десериализации:

function parseNumber(value) {
  const n = Number(value);
  return Number.isFinite(n) ? n : null;
}

const zoom = parseNumber(params.get('z'));

if (zoom && zoom >= 0 && zoom <= 24) {
  map.setZoom(zoom);
}

Также важно использовать encodeURIComponent при генерации URL:

const url = `?lng=${encodeURIComponent(lng)}&lat=${encodeURIComponent(lat)}`;

Защита конфигурации стиля (Style Specification)

Mapbox Style Spec может приходить с сервера или формироваться динамически. Прямая подстановка пользовательских значений в style JSON опасна.

Рискованные элементы:

  • sources
  • layers
  • paint свойства
  • выражения (expressions)

Пример потенциально опасного сценария:

map.setStyle(userStyle);

Без валидации пользователь получает контроль над всей картой.

Подходы к защите:

  • фиксированный набор стилей
  • частичная параметризация (только цвета, только фильтры)
  • валидация схемы JSON

Пример ограничения:

const allowedStyles = {
  streets: 'mapbox://styles/mapbox/streets-v12',
  dark: 'mapbox://styles/mapbox/dark-v11'
};

if (allowedStyles[userChoice]) {
  map.setStyle(allowedStyles[userChoice]);
}

Ограничение объёма данных и защита от DoS

Mapbox GL JS чувствителен к объёму GeoJSON и количеству объектов.

Риски:

  • слишком большие FeatureCollection
  • тысячи маркеров DOM-слоя
  • сложные полигоны с большим числом координат

Базовые ограничения:

const MAX_FEATURES = 500;
const MAX_COORDS = 5000;

Фильтрация:

data.features = data.features.slice(0, MAX_FEATURES);

Безопасная работа с маркерами и DOM-элементами

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

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

Это создаёт XSS-вектор.

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

el.textContent = userInput;

Или генерация структурированного DOM:

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

Контроль сторонних API и проксирование данных

Mapbox-приложения часто агрегируют данные:

  • геокодинг
  • POI API
  • собственные backend endpoints

Любой ответ API должен рассматриваться как недоверенный, особенно если он проксируется напрямую в карту.

Рекомендуется:

  • валидация схемы ответа (JSON Schema / Zod)
  • удаление неожиданных полей
  • ограничение размера ответа
  • нормализация координат

Общая модель безопасного ввода

Безопасная обработка данных в Mapbox GL JS строится на последовательности:

  1. получение входных данных
  2. приведение типов
  3. проверка диапазонов
  4. фильтрация структуры
  5. удаление опасных полей
  6. только затем — передача в Mapbox API

Пренебрежение любым из этих этапов переносит контроль над картой в пользовательский ввод, что особенно критично в приложениях с публичным доступом или интеграцией внешних источников данных.