XXE Injection via Translation File Import in Tolgee
Tolgee's translation import parsers don't disable external entity processing, letting any user with import permissions read arbitrary files from the server and perform SSRF. Confirmed on the cloud platform.
How I Found It
Tolgee is an open-source localization platform. Teams use it to manage translations for their apps, and one of its core features is importing translation files in formats like Android XML, XLIFF, .resx, and Apple stringsdict.
Every one of those formats is XML.
When I see an application that accepts user-uploaded XML files and processes them server-side, the first thing I check is whether the parser disables external entity resolution. If it doesn’t, the XML spec itself becomes the vulnerability. You define a custom entity in a DTD that points to a local file, the parser dutifully resolves it, and the file contents end up wherever the entity was referenced. It’s called XXE, and it’s been in the OWASP Top 10 for years, but it keeps showing up because XML parsers in Java ship with dangerous defaults.
I cloned the Tolgee repo and grepped for XMLInputFactory and DocumentBuilderFactory, the two standard ways to create XML parsers in the JVM ecosystem. Six results. I opened each one and looked for the security properties that should be there: IS_SUPPORTING_EXTERNAL_ENTITIES, SUPPORT_DTD, disallow-doctype-decl.
None of them had any.
The Vulnerable Code
Here’s the Android XML resource importer:
// XmlResourcesProcessor.kt
val inputFactory: XMLInputFactory = XMLInputFactory.newInstance()
inputFactory.createXMLEventReader(context.file.data.inputStream())
That’s it. XMLInputFactory.newInstance() with zero configuration. In Java, this means external entities are enabled by default. The parser will follow SYSTEM entity declarations, resolve file:// and http:// URIs, and substitute the results inline.
The same two lines appeared in five other files:
ResxProcessor.kt(.NET resource files),XMLInputFactory.newInstance()XliffFileProcessor.kt(XLIFF translations),XMLInputFactory.newDefaultFactory()StringsdictFileProcessor.kt(Apple stringsdict), same patternTextToXmlResourcesConvertor.kt(output converter),DocumentBuilderFactory.newInstance()domBuilder.kt(DOM utility),DocumentBuilderFactory.newInstance()in two separate places
Six entry points. Any of them could be reached by uploading a file through the import UI.
Exploitation
The payload is a standard Android XML resource file with a DTD that defines an external entity:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE resources [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<resources>
<string name="test_key">&xxe;</string>
</resources>
When Tolgee’s XmlResourcesProcessor parses this file, it encounters &xxe; and resolves it by reading /etc/passwd from the server’s filesystem. The file contents become the translation value for test_key. No special permissions, no additional requests, no exploitation framework. Just upload and read.
I tested this on app.tolgee.io, Tolgee’s own cloud platform.
What an attacker could read
The file:// scheme gives you arbitrary file read as the application user. On a typical Tolgee deployment, that includes:
/proc/self/environ, the process environment. This is where most containerized apps store database passwords, JWT signing keys, API tokens for third-party services, and the encryption key for stored secrets.- Application config files like
application.yamlor.env, often containing the same secrets in a more readable format. /proc/self/cmdline, which can reveal startup flags, config file paths, and sometimes inline credentials.
SSRF via http:// entities
XXE isn’t limited to local files. You can define an entity with an http:// URI, and the server will make an outbound HTTP request to resolve it:
<!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/iam/security-credentials/">
On AWS, GCP, or Azure, this hits the instance metadata service and returns IAM credentials. Those credentials often have broad access to the cloud account: S3 buckets, databases, other services. A single request through a translation file could give an attacker a foothold in the entire infrastructure.
Impact
The attack surface here is unusually wide for an XXE. Any authenticated user with import permissions on any project can trigger it. On a multi-tenant cloud platform, that’s a low bar. You sign up, create a project, import a file, and you’re reading files from the server that hosts every other tenant’s data.
From file read alone, the realistic attack chain is:
- Read
/proc/self/environto extract database credentials - Connect to the database directly (if network-reachable) or use SSRF to query it through an internal endpoint
- Access every tenant’s translations, API keys, and user data
This is why it scored a CVSS 9.3. Low privilege requirement, no user interaction, network-accessible, and it breaks confidentiality across tenant boundaries.
Remediation
Tolgee shipped a fix in commit 7c71d5a. Rather than patching each file individually, they created a centralized XmlSecurity utility:
object XmlSecurity {
fun newSecureXmlInputFactory(): XMLInputFactory {
val factory = XMLInputFactory.newInstance()
factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false)
factory.setProperty(XMLInputFactory.SUPPORT_DTD, false)
return factory
}
fun newSecureDocumentBuilderFactory(): DocumentBuilderFactory {
val factory = DocumentBuilderFactory.newInstance()
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true)
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true)
factory.setFeature("http://xml.org/sax/features/external-general-entities", false)
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false)
factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false)
factory.isXIncludeAware = false
factory.isExpandEntityReferences = false
return factory
}
}
All six affected files were updated to call XmlSecurity.newSecureXmlInputFactory() or XmlSecurity.newSecureDocumentBuilderFactory() instead of the raw constructors. They also added XxeVulnerabilityTest.kt with test cases covering every import processor, so this class of bug can’t silently regress.
This is the right approach. A central factory means future XML parsers get the secure configuration by default, and the tests make it explicit that XXE prevention is a requirement, not an assumption.
Timeline
-
Vulnerability discovered and reported to Tolgee
-
Public disclosure