Skip to content

BRMO-438: Harden XML parsing and transforming against XXE consistently - #2839

Merged
mprins merged 3 commits into
masterfrom
harden-xml-parsers-and-transformers
Oct 23, 2026
Merged

mprins merged 3 commits into
masterfrom
harden-xml-parsers-and-transformers

Conversation

@matthijsln

@matthijsln matthijsln commented Oct 22, 2026 •

Copy link
Copy Markdown
Member

Summary

Adds SecureXmlFactories (brmo-loader) with hardened factory methods, based on Apache Commons Secure XML, and uses them instead of TransformerFactory.newInstance() / DocumentBuilderFactory.newInstance() everywhere, and XMLInputFactory.newInstance() in brmo-loader.

Commons Secure XML secures each factory with a recipe for the actual implementation (JDK, Xerces, Woodstox, Saxon) and resolves external references (entities, DTDs, stylesheets, document()) to empty content. On top of that:

  • newTransformerFactory(): SecureTransformerFactory plus FEATURE_SECURE_PROCESSING. Stylesheets loaded via a URIResolver (the xsl:imports from the classpath) still work.
  • newDocumentBuilderFactory(): SecureDocumentBuilderFactory plus disallow-doctype-decl, so documents with a DOCTYPE are rejected instead of parsed with empty entities.
  • newXMLInputFactory(): SecureXMLInputFactory plus SUPPORT_DTD=false. Used by GbavXMLReader, Brk2SnapshotXMLReader and DataComfortXMLReader. These already had SUPPORT_DTD=false, so this is for consistency/defence in depth.

The BGT and BAG2 loaders don't depend on brmo-loader and configure their own StAX factory (currently Aalto via SPI, which Commons Secure XML does not list as supported), so there IS_SUPPORTING_EXTERNAL_ENTITIES=false is added inline next to the existing SUPPORT_DTD=false. Both properties are accepted by the JDK parser, Woodstox and Aalto. The follow-up PR #2840 replaces Aalto with Woodstox and uses Commons Secure XML there too.

Fixed XXE

  • NhrXMLReader: the nHR input was parsed by the split transformer as a StreamSource using an unhardened TransformerFactory. Setting source.setSystemId("") did not prevent resolving an absolute file:// external entity. A test showed the file contents were read before this change. Credit to Ray Sabee (Whitehat Security, @raysabee) for finding and reporting this issue!
  • Unhardened DocumentBuilderFactory in RsgbTransformer, RsgbBRPTransformer.merge, NhrXMLReader.extractBSN, Brk2Bericht, GbavBericht and StUFBGasynchroon.

BRPXMLReader, WozXMLReader and RsgbWOZTransformer already had hardening inline; they now use the shared factory. An unused DocumentBuilderFactory in NHRLoadUtils is removed.

Behaviour change

  • XML containing a DOCTYPE is now rejected by all DocumentBuilder parsing (already the case for BRP and WOZ). None of the supported formats use a DTD.
  • The TransformerFactory does not reject a document with an external entity but resolves it to empty content: the Commons Secure XML resolver takes precedence over the JAXP accessExternalDTD/accessExternalStylesheet attributes. The external resource is never read.

Test plan

  • New SecureXmlFactoriesTest: external entity not resolved by the transformer (also with a URIResolver for xsl:import); rejected by the document builder and StAX; billion laughs rejected; plain XML still works
  • Unit tests pass for brmo-loader, bgt-loader, bag2-loader (with Aalto), brmo-stufbg204, nhr-loader, brmo-service and brmo-commandline
  • Integration tests (database profiles), which run the Rsgb*Transformers on real data

🤖 Generated with Claude Code

Add SecureXmlFactories with hardened TransformerFactory and
DocumentBuilderFactory factory methods and use these everywhere instead
of the newInstance() factory methods.

The TransformerFactory has secure processing enabled and does not load
external DTDs or stylesheets (stylesheets via a URIResolver are still
allowed). This fixes XXE in NhrXMLReader, which parsed the input using a
StreamSource directly with an unhardened TransformerFactory; setting an
empty system id on the source did not prevent external entity resolution.

The DocumentBuilderFactory rejects documents with a DOCTYPE, preventing
external entities and entity expansion. This hardens previously
unprotected parsing in RsgbTransformer, RsgbBRPTransformer, NhrXMLReader,
Brk2Bericht, GbavBericht and StUFBGasynchroon.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add SecureXmlFactories.newXMLInputFactory() with DTD and external entity
support disabled and use it in the brmo-loader StAX readers. For the BGT
and BAG2 loaders, which configure their own StAX factory and do not
depend on brmo-loader, also disable external entities inline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@matthijsln matthijsln changed the title Harden TransformerFactory and DocumentBuilderFactory against XXE Harden XML parsing and transforming against XXE consistently Oct 22, 2026
@codecov

codecov Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 80.55556% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 37%. Comparing base (5ae0496) to head (a736883).

Files with missing lines Patch % Lines
...main/java/nl/b3p/brmo/loader/xml/BRPXMLReader.java 0% 3 Missing ⚠️
...va/nl/b3p/brmo/loader/util/RsgbBRPTransformer.java 0% 2 Missing ⚠️
...n/java/nl/b3p/brmo/stufbg204/StUFBGasynchroon.java 0% 2 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff            @@
##             master   #2839   +/-   ##
========================================
- Coverage        37%     37%   -0%     
  Complexity       74      74           
========================================
  Files           199     200    +1     
  Lines         13551   13545    -6     
  Branches       1431    1431           
========================================
- Hits           4909    4904    -5     
+ Misses         8236    8233    -3     
- Partials        406     408    +2     
Files with missing lines Coverage Δ
...3p/brmo/bag2/loader/BAG2GMLMutatieGroepStream.java 86% <100%> (+1%) ⬆️
...java/nl/b3p/brmo/bgt/loader/BGTObjectStreamer.java 82% <100%> (+1%) ⬆️
...in/java/nl/b3p/brmo/loader/entity/Brk2Bericht.java 81% <100%> (ø)
...in/java/nl/b3p/brmo/loader/entity/GbavBericht.java 82% <100%> (ø)
.../nl/b3p/brmo/loader/util/DataComfortXMLReader.java 93% <100%> (-<1%) ⬇️
.../java/nl/b3p/brmo/loader/util/RsgbTransformer.java 70% <100%> (ø)
...va/nl/b3p/brmo/loader/util/RsgbWOZTransformer.java 88% <100%> (-1%) ⬇️
...va/nl/b3p/brmo/loader/util/SecureXmlFactories.java 100% <100%> (ø)
.../nl/b3p/brmo/loader/xml/Brk2SnapshotXMLReader.java 94% <100%> (-<1%) ⬇️
...ain/java/nl/b3p/brmo/loader/xml/GbavXMLReader.java 88% <100%> (-<1%) ⬇️
... and 5 more

... and 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@mprins mprins left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Base the hardened factories on Apache Commons Secure XML, which applies
implementation-specific securing for the JDK, Xerces, Woodstox and Saxon.
The DocumentBuilderFactory and XMLInputFactory still reject documents
with a DOCTYPE.

The TransformerFactory now resolves external entities to empty content
instead of failing, because the library's resolver takes precedence over
the accessExternalDTD/accessExternalStylesheet attributes, so these are
removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@matthijsln
matthijsln added this pull request to stack #2841 October 23, 2026 10:58
@matthijsln matthijsln changed the title Harden XML parsing and transforming against XXE consistently BRMO-438: Harden XML parsing and transforming against XXE consistently Oct 23, 2026
@mprins
mprins merged commit 9b8cb76 into master Oct 23, 2026
28 of 29 checks passed
@mprins
mprins deleted the harden-xml-parsers-and-transformers branch October 23, 2026 13:23
mprins pushed a commit that referenced this pull request Oct 23, 2026
)

## Summary

Stacked on #2839 (base branch `harden-xml-parsers-and-transformers`);
review only the last two commits.

- **Replace Aalto with Woodstox** for the BGT and BAG2 loaders. Aalto
was added for performance, which is not critical anymore: XML parsing
CPU usage is small compared to network and database IO, and loading is
multithreaded. `brmo-service` already excluded Aalto and so already ran
these loaders on Woodstox. `woodstox-core` is now managed at 7.3.0,
which also upgrades the 6.x version pulled in transitively (CXF,
xmlsec).
- **New `StaxMateFactories`** (bgt-loader, also used by bag2-loader):
creates the StaxMate input factory secured by Apache Commons Secure XML
(coalescing, no DTD), and **fails with an `IllegalStateException` if the
StAX implementation does not natively implement Stax2**.

### Why the Stax2 check

StaxMate works with any StAX parser, but for a non-Stax2 parser (such as
the JDK default) it wraps the reader in `Stax2ReaderAdapter`, which
counts the element depth itself in `next()` and `getElementText()` but
not in `nextTag()`.

The GeoTools `XmlStreamGeometryReader` reads geometries directly from
`SMInputCursor.getStreamReader()` and uses `nextTag()`. StaxMate
re-synchronizes cursors using `getDepth()` of the reader, so after a
curved geometry (`gml:Arc`) the depth is off and the cursors **silently
skip all following attributes** of the object. Polygons happen to
balance out, so only the `bgt_wijk_curve.gml` test caught it when Aalto
was removed.

StaxMate allows but discourages bypassing cursors (see
[FasterXML/StaxMate#2](FasterXML/StaxMate#2),
still open). With a native Stax2 parser the depth is tracked by the
parser itself, so this works. The javadoc of `StaxMateFactories`
documents this, and that the check should stay even when StaxMate is
upgraded automatically, unless that issue is verified to be resolved.

## Test plan

- [x] `BGTObjectStreamerTest` (including `bgt_wijk_curve.gml`) and BAG2
loader tests pass with Woodstox
- [x] New `StaxMateFactoriesTest`: native Stax2 reader with Woodstox;
`IllegalStateException` when the JDK StAX implementation is selected
- [x] Each commit builds and passes the unit tests on its own;
brmo-service, nhr-loader and brmo-commandline (CXF with Woodstox 7.3.0)
pass
- [ ] Integration tests (database profiles) loading BGT and BAG2 data

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants