Introduction

During a penetration test, I found an XXE vulnerability in the XML file upload feature of a web application. The initial attempts ended with the same generic error message every time, so at first it was not clear what was happening in the background.

The breakthrough came from examining the application’s own XML export and another feature, the audit log. By uploading a suitably modified file, I managed to get the contents of a file read from the server to appear in one of the audit log’s error messages.

In this environment, the vulnerability made it possible to read both file contents and directory names, which allowed the file system to be mapped. Below I describe the discovery process and the exploitation, proven by reading the /etc/passwd file.

What is XXE?

One feature of XML is that we can define so-called entities in it. These are named pieces of content that can be referenced in other parts of the document. The parser can insert the value belonging to the entity in place of the reference.

The content of an external entity comes from a referenced source. This can be, for example, a resource available at a network address or a local file on the server. So “external” does not necessarily mean an internet source: the data can be on the same server, just outside the XML document.

We speak of XXE — XML External Entity injection — when an attacker can abuse the resolution of external entities through XML they supply. In this way, an improperly configured XML parser can be made to read local files or send network requests. The range of accessible data also depends on the application’s permissions and on how the parser works.

The consequences can include leakage of sensitive files, access to internal services or mapping of the internal network. In special environments, under additional conditions, even code execution may become possible. XML processing can also be exposed to resource exhaustion attacks leading to denial of service.

The first attempts

During the project, I started examining the XML file upload feature specifically looking for an XXE flaw. In the early stage of the test, however, I did not get any usable feedback: the server always returned the same simple error message to my various attempts.

I set the feature aside for a while and continued examining the rest of the application.

Towards the end of the project, in the reporting phase, I returned to it and uploaded a classic “Billion Laughs” XML file. In this attack, entities built on top of each other repeat the content of the previous level. During processing, the originally small input can expand into a very large text, causing significant memory and CPU usage.

After the upload, the website became unavailable for 10–15 minutes.

This suggested that the resource handling of the XML processing was worth investigating further. At the same time, the outage on its own did not yet prove that external entities were being resolved: Billion Laughs can also work with internal entities, without reading an external file or network resource.

After that, I went on to examine whether server-side data could also be read through the feature.

The breakthrough came from the application’s own XML export

To succeed, I had to understand what data the import feature expected.

I got no meaningful result with the ready-made XXE samples available on the internet. It turned out, however, that the application’s audit log — the interface that records operations and errors — displays a more detailed error message if the structure of the XML meets the expectations but one of the processed fields contains an invalid value.

No sample import file was available for the project. So I exported an XML file containing a small amount of data from the application’s data visualisation interface. This provided the structure the application was already able to process properly.

I modified this file so that the value of one of the fields to be processed would come from a local file on the server. The goal was to get the resulting invalid field value into the audit log.

How did the file’s contents end up in the audit log?

In simple examples of file reading via XXE, the data read appears directly in the response to the upload. In this application, the direct response remained a generic error message. The detailed feedback was provided by the audit log.

Inside the document type declaration of the modified XML, that is, the DOCTYPE section, I defined the following external entity:

<!ENTITY example SYSTEM "file:///etc/passwd">

This links the entity named example to the server’s /etc/passwd file. If the parser allows resolution, the content read from the file can take the place of the reference to the entity.

Then, in one of the XML’s data fields that the import feature processed in any case, I placed the following reference:

&example;

This is how the process came together:

  1. The application started processing the correctly structured XML.
  2. The XML parser resolved the example external entity and read the referenced file.
  3. The file’s contents were inserted into the value of the selected data field.
  4. This value did not meet the expectations for the field, so the application reported an error.
  5. The error message also included the problematic value, so the contents of /etc/passwd appeared in the audit log.

The data therefore entered processing through the XML import and became readable on the audit log interface.

/etc/passwd contains data about the system’s local user accounts, such as account names, identifiers and home directory paths. On modern Linux systems, password hashes are typically stored separately, in the /etc/shadow file.

The result shown thus specifically proved local file reading via XXE. The significance of the vulnerability was that the application made server-side data accessible that the import feature should never have exposed.

What did I learn from the case?

The right input structure can be decisive

In this case, the application’s own export helped reach the processing point where the flaw became visible.

The rejection of a generic test file does not on its own show whether the application handles external entities securely. The input may already get stuck at an earlier check. The XML used for testing is therefore worth adapting to the data structure the given feature actually expects.

Feedback can also appear in another feature

The response to the upload was only one part of the process. The audit log made visible a detail that was missing from the direct response.

For me, the case showed that related features have to be examined together. The result of the import, the processing errors and the log entries together can give a picture of what happened in the background.

An external call can be a useful check

If the server can connect outwards, the resolution of external entities can be checked with a reference pointing to a test server under our control. A network request that can be clearly attributed to the test can give feedback on access to the external resource.

However, the absence of a callback does not rule out XXE. The network connection may, for example, be blocked by a firewall, while reading local files remains possible. In such cases, examining the application’s responses, error messages and the accessible logs can provide further clues.

The limits of testing internal entities

After the case, it occurred to me that a smaller, less resource-intensive “XML bomb” might be usable as an early indicator, especially when outbound connections are prohibited.

This conclusion, however, needs to be refined: expanding internal entities and resolving external entities are separate capabilities. One working does not automatically prove the other.

The short example below illustrates the stacked expansion of internal entities:

<!DOCTYPE attack [
  <!ENTITY lol "lol">
  <!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
  <!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
]>
<attack>&lol3;</attack>

The example contains no reference to an external file or network address, so successful processing does not on its own prove an XXE vulnerability. This is an illustrative XML; examining a specific import feature also requires taking its expected structure into account.

A simple, small entity substitution may be enough for a basic check of entity handling. Load-increasing probes are a separate investigation; a shorter “XML bomb” cannot automatically be considered a safe fallback either.

Handling of the & character is only one detail

Because of entity references such as &example;, the handling of the & character can also affect the result of the test. From a rejected input, however, it cannot immediately be determined whether this character is filtered, the use of DOCTYPE is prohibited, or some other check stops the processing.

Banning a character cannot on its own be considered complete XXE protection. Defence has to be built on the XML parser’s configuration and on controlling access to external resources.

Besides timing, the environment matters too

The attempt described took place at the end of the project, when an outage would have hindered the rest of the testing less. This did not, however, eliminate the risk to the operation of the client’s system.

Tests that put load on the service should therefore be carried out at a time agreed in advance, within defined limits, preferably not in a production environment. The aim is for proving the vulnerability to have the smallest necessary impact.

Defence options

Configuring the XML parser securely

The primary defence is to disable the resolution of external entities and the loading of external DTDs. If the application does not need document type definitions, it is also advisable to reject DOCTYPE declarations.

In a Java environment, for example, with suitable DOM and SAX parsers the disallow-doctype-decl setting can be used to prohibit DOCTYPE. The effect of XMLConstants.FEATURE_SECURE_PROCESSING depends on the parser and the configuration, so it cannot be treated as a single switch for XXE protection in every environment. Control of external access and processing limits must also be checked.

In PHP, libxml_disable_entity_loader(true) is an older solution; the function has been deprecated since PHP 8.0. From libxml 2.9, entity substitution is disabled by default, but the processing options used can affect this. For proper protection, the actual PHP and libxml versions and the application’s settings therefore have to be examined together.

Schema validation and allowed resources

Schema validation is useful for checking the structure and data content of uploaded XML, but on its own it does not disable the resolution of external entities.

If external resources are needed for business reasons, access to them must be tied to a narrow allowlist, preferably using verified local copies. External access by schema validation and other XML processing steps must also be controlled.

The type and content of uploaded files must also be checked on the server side; client-side validation is a supplementary measure.

Updates, resource limits and permissions

XML parser libraries must be kept up to date. In addition to prohibiting external access, limits on the size of processable input, on entity expansion and on resource usage may also be needed to reduce denial-of-service risks.

The principle of least privilege is also important: the process running the application should only have access to the files and network resources it needs to function. This also limits the possible impact of a successful exploitation.

Protecting error messages and the audit log

In this case, it was the audit log that made the data read accessible, so the fix also has to cover logging.

It must be reviewed which field values end up in error messages, what sensitive data can appear in the log, and who can access these entries. Logging should help troubleshooting without creating further opportunities for data exposure.

Conclusion

The most important lesson of the case is that a generic error message does not tell you what happened in the background.

To uncover the vulnerability, I had to understand the XML structure the application expected and follow the result of the processing all the way to the audit log. The application’s own export provided the right input, and the log made the data read from the server visible.

For effective testing, the upload, data processing, error messages and related interfaces should therefore be examined together. The fix must review the same complete process: from the XML parser’s settings, through the application’s permissions, to the data appearing in the log.

Sources and further reading