Skip to content

COMMONSXML-5: SAXParserFactory: capability-driven hardening - #11

Merged
garydgregory merged 2 commits into
apache:mainfrom
ppkarwasz:feature/sax-feature-based-hardening
Jul 5, 2026
Merged

COMMONSXML-5: SAXParserFactory: capability-driven hardening#11
garydgregory merged 2 commits into
apache:mainfrom
ppkarwasz:feature/sax-feature-based-hardening

Conversation

@ppkarwasz

Copy link
Copy Markdown
Member

Applies the COMMONSXML-1 capability-driven approach to the SAX path, replacing the per-implementation class-name dispatch for SAXParserFactory and XMLReader with a single SAXParserHardener.

  • harden(SAXParserFactory) sets FSP and wraps the factory in the now-universal HardeningSAXParserFactory; hardenReader(XMLReader) applies the limits, skips the external DTD subset where supported, and probes ACCESS_EXTERNAL_* (bare reader when honoured, deny-all EntityResolver otherwise).
  • HardeningSAXParserFactory returns a HardeningSAXParser exposing both the hardened XMLReader and a matching SAX 1 Parser, so either access path is hardened.
  • StockJdkProvider/XercesProvider lose their SAX recipes; Limits gains tryApply(XMLReader); JaxpSetters gains setOptionalFeature(XMLReader) / trySetProperty(XMLReader). AndroidProvider is removed.
  • Android/Expat keeps a documented class-name exception with two fixups: a subset-aware deny-all resolver (fail on external entities, allow an unused external subset) and an ExpatReaderWrapper that rejects namespace-prefixes eagerly. Android DOM (KXmlParser) stays a pass-through.

Behaviour change: the returned SAXParserFactory no longer forces namespace awareness (the previous recipes did); it is left to the caller. XmlFactories.harden(Source) sets it where the schema/XSLT/validator source paths need it.

Tests: adds SchemaLocationSaxTest and a SAX case in UnsupportedXmlImplementationTest; SAX attack assertions remain strict (Android still blocks). mvn verify green across stock JDK and external Xerces; the Android/Expat fixups still need a run on the api33 managed device.

Replace the per-implementation class-name dispatch for SAX with a single
capability-driven recipe, mirroring the DocumentBuilderFactory rework. A new
SAXParserHardener probes feature and property support and adapts:

- harden(SAXParserFactory) sets FSP (skipped on Android's Expat, which rejects
  it) and wraps the factory in the now-universal HardeningSAXParserFactory,
  since SAXParserFactory exposes no property API.
- hardenReader(XMLReader) sets FSP, skips the external DTD subset where
  supported, applies limits and probes ACCESS_EXTERNAL_*: the bare reader is
  returned when the JAXP 1.5 properties are honoured (stock JDK), otherwise a
  deny-all EntityResolver is installed (external Xerces).
- Limits.tryApply(XMLReader) picks the Xerces SecurityManager vs JDK
  limit-property path by class name (the JDK reader is itself a Xerces fork
  exposing the same property, so a probe cannot tell them apart).
- JaxpSetters gains setOptionalFeature(XMLReader) and trySetProperty(XMLReader).
  StockJdkProvider and XercesProvider lose their SAX recipes; XercesProvider now
  handles only Schema factories.

HardeningSAXParserFactory becomes a single-argument wrapper that returns a
HardeningSAXParser, which exposes the hardened reader and a matching SAX 1
Parser (cast or XMLReaderAdapter, lazily) so consumers reaching the parser
through either path, including Xalan's identity transformer, get the same
hardening.

Android keeps its fixups rather than becoming a pass-through. Expat ignores
external fetches silently when no resolver is set, so a subset-aware deny-all
resolver is installed to fail on external entities while still letting an unused
external subset load, and an ExpatReaderWrapper rejects the unsupported
namespace-prefixes feature eagerly instead of failing mid-parse. DOM
(KXmlParser) remains a pass-through.

Behaviour change: the returned SAXParserFactory no longer forces namespace
awareness. The previous StockJdkProvider and XercesProvider SAX recipes called
setNamespaceAware(true); whether to parse namespace-aware is the caller's
decision, not a hardening concern, so it is now left at the implementation
default (DOM never set it). The internal Source-parsing path keeps it on:
XmlFactories.harden(Source) sets namespace awareness itself, since schema, XSLT
and validator sources are parsed in a namespace-aware context.

Adds SchemaLocationSaxTest (the SAX counterpart of SchemaLocationDomTest,
reusing AttackTestSupport's shared strict reporter) and a SAX case in
UnsupportedXmlImplementationTest. AndroidProvider is removed.

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

Copy link
Copy Markdown
Member

The Jira ticket is COMMONSXML-5

@garydgregory
garydgregory merged commit fa3002c into apache:main Jul 5, 2026
15 checks passed
@ppkarwasz
ppkarwasz deleted the feature/sax-feature-based-hardening branch July 5, 2026 18:31
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.

2 participants