While manually reviewing source code for some unrelated work, I found an A -> B -> A reference chain that crashed the server. The code looked like it had come from somewhere else, so I followed it with some OSINT and eventually traced it back to docx4j’s org.docx4j.model.PropertyResolver.
The original code recursively followed the style chain without detecting that it had already visited the same style. For server-side applications that process Word documents, one uploaded DOCX can therefore end the processing thread with a StackOverflowError.
CVE-2026-53752
Affected Versions
| Detail | Information |
|---|---|
| Package | org.docx4j:docx4j-core |
| Affected versions | Up to and including 11.5.13 |
| Fixed version | 11.5.14 |
| CVSS | 7.5 (High) — CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H |
| Weaknesses | CWE-674 (Uncontrolled Recursion); CWE-770 (Allocation of Resources Without Limits or Throttling) |
Introduction
docx4j is a Java library for reading, creating, and converting Office Open XML documents. It is the sort of dependency that ends up behind an Upload feature and is quietly used by at least 200K applications monthly.
Word stores its style definitions in word/styles.xml. A style can inherit properties from another style using w:basedOn. For example, a heading may be based on the Normal paragraph style and override only the properties it needs.
Resolving the final formatting means walking from one style to its parent, then to that parent’s parent, and merging the properties along the way. Normal Word documents make this look like a tree. A hand-written DOCX does not have to behave like one.
Nothing stops the style relationships from looking like this:
Normal -> StyleA -> StyleB -> Normal
Once a file format lets one object refer to another, the parser has to treat those references as an untrusted graph. docx4j was treating them as a trusted hierarchy.
Bug Discovery
I found this while doing unrelated work. I was doing source code review (yes, still mostly manually) and found a snippet of code that referenced A to B to A and managed to crash the server. I wondered where the developer got the code from (as we all know that no one writes from scratch these days) and eventually with enough OSINT, discovered docx4j and followed the code into org.docx4j.model.PropertyResolver.
The paragraph-property walker made the problem obvious:
private void fillPPrStack(String styleId, Stack<PPr> pPrStack) {
Style style = liveStyles.get(styleId);
...
if (style.getBasedOn() == null) {
log.debug("Style " + styleId + " is a root style.");
} else if (style.getBasedOn().getVal() != null) {
String basedOnStyleName = style.getBasedOn().getVal();
fillPPrStack(basedOnStyleName, pPrStack);
...
fillPPrStack takes the current style’s w:basedOn value and calls itself with the parent style. There was no list of previously visited style IDs and no maximum depth.
With A -> B -> A, the calls become:
fillPPrStack("A")
fillPPrStack("B")
fillPPrStack("A")
fillPPrStack("B")
...
It only stops when the thread stack is exhausted and the JVM throws java.lang.StackOverflowError.
There was even an old commented-out check for a style directly referring to itself in the run-property walker, fillRPrStack. That would only have handled A -> A anyway. A two-style or longer cycle needs proper cycle detection.
The same unsafe assumption appeared in the paragraph, run, and table style walkers, along with style-tree and TOC-related code that also followed w:basedOn recursively.
The Exploit
The DOCX body can be a single character. The trigger lives in word/styles.xml, and the default Normal style can be placed inside the cycle. That allows the bad chain to be reached while PropertyResolver is being initialized, before meaningful document content is processed.
MainDocumentPart#getPropertyResolver() creates the resolver lazily. A lot of normal docx4j functionality eventually asks for effective style properties, including common HTML/PDF conversion and TOC-related paths. An application does not need to call some obscure parser option to hit the bug.
I supplied a working proof of concept to Plutext during coordinated disclosure. I am not publishing the DOCX generator yet while related findings complete disclosure. The structure of the trigger is already public in the GitHub advisory: put a cycle in the w:basedOn chain and load the document through a code path that resolves styles.
Impact
At minimum, the crafted document causes the current processing thread to fail with StackOverflowError. What happens to the service after that depends on how the application isolates document processing.
Authentication and worker isolation affect the realistic severity. An application that catches the failure or performs conversion in a disposable worker may turn it into one failed request. A less isolated service may lose workers or repeatedly tie up its processing pool when the file is submitted again, leading to denial of service.
This is mainly relevant to applications that process DOCX files from untrusted users: document converters, document-management and e-discovery systems, search indexers, template importers, and attachment-processing services. The malformed relationship is made from ordinary OOXML style elements, so generic file scanning is unlikely to understand that the style graph is cyclic.
The Fix
The fix in 11.5.14 tracks the style IDs already seen during each recursive walk. If a style appears for a second time, resolution stops instead of following the cycle again.
Plutext also added a limit for style chains deeper than 32 entries. The shared check is roughly:
if (seen.contains(styleId) || seen.size() > 32) {
log.warn("Cycle detected in style basedOn hierarchy for: " + styleId);
return true;
}
The change was applied to the paragraph, run, and table property stacks, StyleTree, and the TOC StyleBasedOnHelper. By default docx4j logs the problem and degrades gracefully using the properties resolved before the cycle. Applications can instead enable docx4j.openpackaging.exceptions.CyclicStylesException.throw if they want cyclic styles to reject the package with a CyclicStylesException.
This is the perfect fix: detect repeated nodes, place a reasonable bound on attacker-controlled depth, and let the caller decide whether to recover or reject the document.
Credit to Plutext for fixing it quickly. I reported the issue on 25 May, and version 11.5.14 was released on 2 June.
Afterthoughts
This was not a complicated vulnerability. It survived decades because normal documents do not contain cyclic style inheritance, so the happy-path tests never forced the recursive code to visit the same style twice.
The same pattern appears everywhere: style inheritance, imports, parent-child records, symlinks, dependencies, and any other data format where one item can point at another. If the relationships came from an untrusted file, it is a graph even when the specification describes it like a tree.
Some of the other libraries I checked were already safe because they flattened inheritance or resolved it without recursive traversal. So this is completely avoidable. Somebody just has to test A -> B -> A once.
If this research helped you find a CVE in other solutions, I’d be glad if you could link a reference to this blog post.
Timeline
2026-05-25 — Reported to Plutext by email and through a private GitHub Security Advisory
2026-06-02 — Plutext released docx4j 11.5.14 with cycle detection and a style-depth limit
2026-07-07 — GHSA-gc95-3vw8-vg43 published
2026-08-26 — Writeup published