Skip to content

COMMONSXML-1: DocumentBuilderFactory: capability-driven hardening - #5

Merged
ppkarwasz merged 8 commits into
apache:mainfrom
ppkarwasz:feature/document-builder-hardening
Jun 27, 2026
Merged

COMMONSXML-1: DocumentBuilderFactory: capability-driven hardening#5
ppkarwasz merged 8 commits into
apache:mainfrom
ppkarwasz:feature/document-builder-hardening

Conversation

@ppkarwasz

@ppkarwasz ppkarwasz commented Jun 21, 2026

Copy link
Copy Markdown
Member

Summary

Capability-driven hardening for DOM (DocumentBuilderFactory). Tracked as COMMONSXML-1.

1. Capability-driven hardening

Implements @elharo's suggestion in the xml-commons-dev@xerces thread:

Why not set all relevant features and properties on each and check that you succeeded in configuring a minimal set to provide security?

Replaces the per-implementation class-name dispatch for DOM with a single capability-driven recipe:

  • Secure processing (FSP) is required; the external-DTD subset is skipped where supported.
  • Whether the JAXP 1.5 accessExternal* properties are honoured then decides whether the bare factory is already safe or needs a deny-all resolver wrapper. This is the only point where the stock JDK and the external Xerces distribution diverge.
  • Android stays untouched (its parser exposes no hardening surface).
  • An implementation is no longer rejected for being unrecognised: any parser that accepts secure processing and either the access properties or the resolver wrapper satisfies the contract. Only a parser that refuses secure processing now fails.
  • Adds a test that an xsi:schemaLocation hint is not fetched during DOM-side XSD validation (gated on parsers that honour accessExternalSchema).

2. Minimal shading entry point

Adds a public DocumentBuilderHardener.newInstance() so consumers that only need a hardened DocumentBuilderFactory can shade the library and copy a minimal set of classes.

  • jdependency (the engine maven-shade-plugin's minimizeJar uses) works at class granularity, so the shaded set is the transitive closure of this one class.
  • A deny-all EntityResolver is installed as a local lambda instead of reusing Resolvers, which drops the whole Resolvers nested-class tree from the closure (12 -> 7 class files).
  • XmlFactories.newDocumentBuilderFactory() is routed through the new entry point.
  • ShadingFootprintTest uses jdependency to pin the reachable set to those 7 classes, so the footprint cannot silently grow back toward the full library.
  • Javadoc on both methods documents how to enable XInclude: it is held off by the deny-all external-fetch behavior, not the awareness flag, so enabling it also requires a custom EntityResolver.

Testing

mvn -o test (all 7 JAXP-combination executions) passes with 0 failures and 0 errors. The 2 skips are SchemaLocationDomTest under external Xerces, which does not honour accessExternalSchema.

Follow-ups (separate PRs)

  • The minimal shading entry point (struck above), to be filed as its own issue.
  • A deeply-nested-document (element-depth) test, to cover the limit FSP leaves unbounded on JDK 8-21.

🤖 Generated with Claude Code

This change replaces the per-implementation class-name dispatch for DOM with a single capability-driven recipe. Secure processing is required and the external-DTD subset is skipped where supported; whether the JAXP 1.5 accessExternal properties are honoured then decides whether the bare factory is already safe or needs a deny-all resolver wrapper, which is the only point where the stock JDK and the external Xerces distribution diverge. Android stays untouched because its parser exposes no hardening surface.

An implementation is no longer rejected for being unrecognised. Any parser that accepts secure processing and either the access properties or the resolver wrapper satisfies the contract on its own, so only a parser that refuses secure processing now fails.

A new test covers the previously unverified guarantee that an xsi:schemaLocation hint is not fetched during DOM-side XSD validation, gated on parsers that honour accessExternalSchema.

Assisted-By: Claude Opus 4.8 <noreply@anthropic.com>
Expose DocumentBuilderHardener with a public newInstance() so consumers that
only need a hardened DocumentBuilderFactory can shade the library and copy a
minimal set of classes. jdependency (the engine maven-shade-plugin's
minimizeJar uses) works at class granularity, so the shaded set is the
transitive closure of this one class.

To keep that closure small, install a deny-all EntityResolver as a lambda
local to DocumentBuilderHardener instead of reusing Resolvers.DenyAll.ENTITY2.
EntityResolver's resolveEntity(publicId, systemId) hook is consulted for the
external DTD subset and every external entity, so it blocks all external
fetches on the DOM path just as well, while dropping the whole Resolvers
nested-class tree from the closure (12 -> 7 class files).

Route XmlFactories.newDocumentBuilderFactory() through the new entry point, and
add ShadingFootprintTest, which uses jdependency to pin the reachable set to
exactly those 7 classes so the footprint cannot silently grow back toward the
full library.

Assisted-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The androidTest source set compiles all of ../src/test/java against
android.jar, which broke on two tests new to this branch:

- ShadingFootprintTest needs org.vafer.jdependency (a Maven-only test
  dependency) and pins the shading footprint, which is meaningless on
  Android. Exclude it from the Android test compilation.

- SchemaLocationDomTest referenced XMLConstants.ACCESS_EXTERNAL_SCHEMA,
  which android.jar does not expose. Inline the constant's value
  (verified equal to the JDK constant); the test still skips at runtime
  on Android, since the parser does not honour that property.

Verified on the api33 (Pixel 6a) managed device: 77 tests, 0 failures,
0 errors, 17 assumption-gated skips.

Assisted-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rnalSchema

The test exercises JAXP 1.2 XSD validation, which requires the SCHEMA_LANGUAGE
property; that, not accessExternalSchema, is the capability a parser must have
to run it (Android lacks it). Gate both methods on supportsSchemaLanguage().

The hardened assertion also assumed the block was attributed to
accessExternalSchema, which only holds on the stock JDK. External Xerces does
not honour that property and instead blocks the schema fetch through the
deny-all resolver, with a different message. Assert only that the failure
references the external schema, not the mechanism.

Two effects:
- The test now runs and passes on external Xerces too (covering the
  resolver-based schema-block path), where it previously skipped.
- It no longer references XMLConstants.ACCESS_EXTERNAL_SCHEMA, so the
  android.jar compile gap is resolved by this redesign rather than by
  inlining the constant.

Verified: mvn -o test green across all executions; SchemaLocationDomTest runs
2/2 with 0 skips on both stock JDK and external Xerces.

Assisted-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@elharo elharo left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This needs an issue and discussion before PR for each change. This looks like more than one thing. Exactly one change per PR please.

Split per review: this PR (COMMONSXML-1) keeps only the capability-driven
DocumentBuilderFactory hardening. The minimal shading entry point (public
DocumentBuilderHardener.newInstance(), the inlined deny-all resolver,
ShadingFootprintTest and the jdependency test dependency) will be filed as
its own issue.

Reverses 68ad1d1 and 90ef0ae: DocumentBuilderHardener is package-private
again and harden() reuses Resolvers.DenyAll.ENTITY2;
XmlFactories.newDocumentBuilderFactory() calls harden() directly.

Assisted-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ppkarwasz ppkarwasz changed the title DocumentBuilderFactory: capability-driven hardening and a minimal shading entry point COMMONSXML-1: DocumentBuilderFactory: capability-driven hardening Jun 21, 2026
@ppkarwasz

Copy link
Copy Markdown
Member Author

Hi @elharo,

I rolled back the changes regarding shading and created a COMMONSXML-1 issue, where we can discuss the changes.

@elharo elharo left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

trivial nits on semicolons but LGTM

Comment thread src/main/java/org/apache/commons/xml/Limits.java Outdated
Comment thread src/main/java/org/apache/commons/xml/XercesProvider.java Outdated
Comment thread src/main/java/org/apache/commons/xml/XercesProvider.java Outdated
@ppkarwasz
ppkarwasz requested a review from elharo June 21, 2026 17:03

@garydgregory garydgregory 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.

Thanks @ppkarwasz

@ppkarwasz
ppkarwasz merged commit bfa4a5a into apache:main Jun 27, 2026
15 checks passed
@ppkarwasz
ppkarwasz deleted the feature/document-builder-hardening branch June 27, 2026 17:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants