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

Источники пользовательских данных в картографических приложениях

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

Особую опасность представляют данные, которые впоследствии интерполируются в HTML-контекст Leaflet-компонентов: попапов, tooltip’ов, кастомных маркеров и control-интерфейсов.


Основные классы угроз при работе с Leaflet

XSS через Popup и Tooltip

Leaflet позволяет привязывать HTML-контент к объектам карты:

marker.bindPopup("<b>Объект</b><br>" + userInput);

При отсутствии санитизации пользовательский ввод может содержать исполняемый HTML и Jav * aScript:

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

В результате контент попапа становится вектором XSS-атаки.

Особенность Leaflet заключается в том, что он не выполняет автоматическое экранирование строк при использовании HTML-режима.


Инъекции через GeoJSON

GeoJSON-слои часто формируются на основе внешних данных:

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

Если feature.properties.description не очищается, любой HTML или скрипт, встроенный в GeoJSON, становится частью DOM.


Манипуляции через URL и параметры запроса

Часто координаты, фильтры и идентификаторы объектов берутся из URL:

const type = new URLSearchParams(location.search).get("type");

Дальнейшее использование таких параметров в построении HTML без проверки приводит к внедрению нежелательного содержимого в интерфейс карты.


Инъекции в кастомные контролы

Leaflet позволяет создавать пользовательские контролы:

L.control.custom = L.Control.extend({
  onAdd: function () {
    const div = L.DomUtil.create("div", "custom-control");
    div.innerHTML = userHtml;
    return div;
  }
});

Использование innerHTML с внешними данными формирует прямую поверхность атаки на DOM.


Принципы безопасной работы с HTML в Leaflet

Отказ от интерпретации HTML

Наиболее надёжный подход — исключение HTML-контента из пользовательских данных:

marker.bindPopup(L.Util.escapeHTML(userInput));

Если используется Leaflet без дополнительных утилит, экранирование должно быть реализовано вручную либо через стандартные механизмы платформы.


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

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

const div = L.DomUtil.create("div", "popup");
div.textContent = userInput;

textContent гарантирует интерпретацию данных как текста, исключая выполнение HTML.


Санитизация через DOMPurify

Для случаев, когда допустим ограниченный HTML (например, жирный текст, ссылки), применяется строгая фильтрация:

const clean = DOMPurify.sanitize(userInput, {
  ALLOWED_TAGS: ["b", "i", "a"],
  ALLOWED_ATTR: ["href"]
});

marker.bindPopup(clean);

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


Безопасная работа с GeoJSON

GeoJSON требует особого внимания, так как часто поступает из внешних API.

Фильтрация свойств объектов

function sanitizeProperties(properties) {
  return {
    name: String(properties.name),
    description: DOMPurify.sanitize(properties.description, {
      ALLOWED_TAGS: [],
      ALLOWED_ATTR: []
    })
  };
}

Изоляция отображаемых данных

L.geoJSON(data, {
  onEachFeature: function (feature, layer) {
    const props = sanitizeProperties(feature.properties);

    layer.bindPopup(`
      <div>
        <strong>${props.name}</strong>
        <p>${props.description}</p>
      </div>
    `);
  }
});

Даже при использовании шаблонных строк сохраняется контроль над содержимым.


Защита при работе с пользовательскими контролами

Любые элементы интерфейса Leaflet-карты, формируемые динамически, должны избегать прямой вставки HTML:

const container = L.DomUtil.create("div", "search-box");

const input = L.DomUtil.create("input");
input.type = "text";

input.addEventListener("input", (e) => {
  handleSearch(e.target.value);
});

container.appendChild(input);

Использование DOM API вместо HTML-строк снижает риск внедрения скриптов.


Обработка URL-параметров

Любые параметры из строки запроса требуют строгой типизации и валидации:

const params = new URLSearchParams(location.search);

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

if (Number.isFinite(lat) && Number.isFinite(lng)) {
  map.setView([lat, lng], 13);
}

Отсутствие проверки типов открывает возможность передачи некорректных значений, влияющих на поведение карты.


Белые списки как основной механизм защиты

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

const allowedLayers = ["roads", "buildings", "parks"];

if (allowedLayers.includes(layerName)) {
  addLayer(layerName);
}

Запрещающий подход (blacklist) менее надёжен, так как не учитывает неизвестные векторы атак.


Изоляция внешнего HTML-контента

Если данные приходят из сторонних сервисов и содержат HTML, их следует изолировать в безопасной области DOM:

const wrapper = document.createElement("div");
wrapper.appendChild(
  DOMPurify.sanitize(externalHtml, { RETURN_DOM_FRAGMENT: true })
);

popup.setContent(wrapper);

Такой подход предотвращает прямую инъекцию в основной документ.


Безопасность при использовании SVG-маркеров

SVG-маркеры могут содержать встроенные события:

const icon = L.divIcon({
  html: `<svg oncl ick="stealData()">...</svg>`
});

Любые inline-обработчики событий рассматриваются как критический риск. Безопасная реализация исключает их использование и формирует SVG через API или безопасные шаблоны.


Контроль данных в слоях и тайлах

URL тайловых слоёв также может стать источником подмены:

L.tileLayer(userTileUrl, {
  attribution: "Map data"
}).addTo(map);

В случае отсутствия контроля возможна подмена источника тайлов на вредоносный сервер. Использование фиксированного списка провайдеров исключает подобный сценарий.


Валидация и нормализация данных

Любые координаты, строки описаний и идентификаторы проходят этап нормализации:

function normalizeString(value) {
  return String(value)
    .replace(/[<>]/g, "")
    .trim()
    .slice(0, 500);
}

Ограничение длины и удаление опасных символов снижает вероятность внедрения payload’ов.


Разделение уровней ответственности

Обработка данных разделяется на три уровня:

  • Слой получения данных — без доверия к источнику
  • Слой валидации — проверка типов и диапазонов
  • Слой отображения — только безопасный, очищенный контент

Такая архитектура предотвращает попадание необработанных данных в DOM Leaflet-карты.


Логика безопасного формирования popup-контента

function createPopupContent(data) {
  const name = normalizeString(data.name);
  const desc = DOMPurify.sanitize(data.description, {
    ALLOWED_TAGS: ["b", "i"],
    ALLOWED_ATTR: []
  });

  const container = document.createElement("div");

  const title = document.createElement("strong");
  title.textContent = name;

  const text = document.createElement("p");
  text.innerHTML = desc;

  container.appendChild(title);
  container.appendChild(text);

  return container;
}

Ограничение доверия к сторонним плагинам

Экосистема Leaflet включает большое количество сторонних расширений. Любой плагин, работающий с HTML или внешними данными, рассматривается как потенциальный источник уязвимостей. Особое внимание требуется при использовании плагинов, изменяющих popups, markers или tile rendering.


Защита на уровне архитектуры приложения

Наиболее устойчивые решения используют:

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

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