Piotr Karwasz created CAMEL-24664:
-------------------------------------

             Summary: Use Apache Commons Secure XML for JAXP factory creation
                 Key: CAMEL-24664
                 URL: https://issues.apache.org/jira/browse/CAMEL-24664
             Project: Camel
          Issue Type: Improvement
          Components: camel-core, camel-validator, camel-xslt
            Reporter: Piotr Karwasz



Camel hardens JAXP by hand. Around forty call sites each configure their own 
subset of {{setFeature}} / {{setProperty}} calls, and the subsets disagree:

* {{XmlConverter.createDocumentBuilderFactory()}} omits 
{{external-parameter-entities}} and {{load-external-dtd}}; 
{{createSAXParserFactory()}} omits {{disallow-doctype-decl}}.
* {{XMLConverterHelper}} sets {{disallow-doctype-decl}} twice, the second block 
added by CAMEL-18825 directly beneath an identical one that had been there 
since 2014.
* {{ProcessorDetailDevConsole}} sets neither {{FEATURE_SECURE_PROCESSING}} nor 
{{load-external-dtd}}.
* Most sites swallow {{ParserConfigurationException}}, so a parser that rejects 
a feature ends up hardened by accident rather than by contract.

The hardening accreted over twelve years in at least six waves (CAMEL-6933 in 
2013, CAMEL-7123 and CAMEL-7131 in 2014, CAMEL-7713 in 2014, CAMEL-10894 in 
2017, CAMEL-12444 in 2018, CAMEL-18825 in 2022). Each wave patched whichever 
sites its author happened to be looking at. The threat model in 
{{docs/user-manual/modules/ROOT/pages/security-model.adoc}} records the cost of 
that drift: CVE-2015-0263 (XML converter in camel-core), CVE-2015-0264 (XPath 
language), CVE-2017-5643 (Validation component) and CVE-2018-8027 (XSD 
validation processor) are all in the XXE / remote-DTD class, and all land in 
the code this issue touches.

h1. Proposal

Create every JAXP factory through [Apache Commons Secure 
XML|https://commons.apache.org/proper/commons-secure-xml/] 
({{org.apache.commons:commons-secure-xml}}, 1.0.0, Java 8+, no transitive 
compile dependencies, Saxon-HE optional/provided, OSGi bundle), and delete the 
hand-rolled feature blocks it subsumes.

h2. Feature-based hardening versus a resolver floor

This is the substantive change, and it is not a like-for-like swap.

|| || Today (feature based) || Proposed (resolver floor) ||
| Mechanism | a set of {{setFeature}} / {{setProperty}} calls per factory | a 
non-removable ignore-all resolver installed under the parser |
| Coverage | whatever the implementation on the classpath honours | identical 
on the stock JDK, Xerces, Woodstox, Saxon and Android |
| Default | varies per site, per implementation | external references are *not 
fetched*, everywhere |
| Failure mode | implementation-specific error, or a silent fetch | the 
reference resolves to *empty content* |
| Evidence | read the flag back with {{getFeature}} | observe what the parser 
actually fetches |

Every factory returned by the library carries a floor that refuses the whole 
family of external resource fetches: the external DTD subset, general and 
parameter entities, {{xs:import}} / {{xs:include}} / {{xs:redefine}} and 
{{xsi:schemaLocation}}, {{xsl:import}} / {{xsl:include}} and {{document()}}, 
{{XInclude}}, and the URI-fetching XPath functions {{doc()}}, {{collection()}} 
and {{unparsed-text()}}.

*The floor cannot be removed, and a user resolver is layered in front of it 
rather than replacing it.* {{setEntityResolver}}, {{setURIResolver}}, 
{{setResourceResolver}} and {{setXMLResolver}} install a _delegate that is 
consulted first_; returning a non-null result opts that specific reference in, 
and anything left unresolved falls through to the floor. So the extension point 
is unchanged in shape, and every resolver Camel already installs 
({{XsltUriResolver}}, {{DefaultLSResourceResolver}}, a user's own) keeps 
working exactly as before.

Two consequences worth stating up front:

* An unresolved reference yields *empty content rather than an error*. A parse 
that used to fail loudly may now continue with nothing in place of the 
resource. The library offers 
{{-Dorg.apache.commons.xml.secure.throwOnUnresolved=true}} to reject instead, 
but it is JVM-global, so Camel cannot set it on a user's behalf.
* Reading a security feature back off a factory no longer tells you whether the 
parser will fetch, because the blocking moved beneath the flag. Assertions must 
move from factory configuration to parse behaviour.

h2. Why this is the right direction

The prevailing philosophy in JAXP is _allowed unless forbidden_, and every 
forbidding knob is optional, implementation-specific and easy to forget. The 
floor inverts it to
*forbidden unless allowed*, which is the property that would have prevented the 
four CVEs above: a new consumer, converter or data format is secure because it 
did nothing, rather than insecure because its author missed a flag.

For deployments that need a real external-resource policy rather than an 
all-or-nothing switch, [XML Resolver|https://www.xmlresolver.org/index.html] is 
a good companion: it implements OASIS catalogs and can be installed as the 
delegate in front of the floor, giving a curated allow-list instead of a 
blanket opt-in.

h1. Scope

One sub-task per JAXP factory. Listed in increasing order of effort rather than 
in the order of the JAXP API, because the later ones depend on decisions taken 
in the earlier.

h2. 1. SAX

The easiest, and the one with no user-facing decision left to take.

The 4.23 upgrade guide (_camel-core - XmlConverter SAX parser factory_) already 
documents the escape hatch, and it is not a feature flag:

{quote}Routes that genuinely need to resolve an external DTD or parameter 
entity through this converter must supply their own {{SAXParserFactory}}.{quote}

That statement stays true verbatim under the floor, so no new escape hatch and 
no documentation change is required. The work is:

* {{SAXParserFactory.newInstance()}} -> 
{{SecureSAXParserFactory.newNSInstance()}} (the {{NS}} variant folds in the 
existing {{setNamespaceAware(true)}}).
* Delete {{FEATURE_SECURE_PROCESSING}}, {{external-general-entities}}, 
{{external-parameter-entities}} and {{nonvalidating/load-external-dtd}}, 
together with the {{try}} / {{catch}} scaffolding around them.

{{disallow-doctype-decl}} is deliberately not set on the SAX path today, and 
does not need to be: the floor makes a DOCTYPE harmless without rejecting the 
document.

Sites: {{XmlConverter.createSAXParserFactory()}}, {{XmlLineNumberParser}}, 
{{XmlPrettyPrinter}}, {{RouteCoverageXmlParser}}, 
{{SchematronProcessorFactory}}, {{DefaultBulkApiClient}}.

h2. 2. DOM

Harder, because the documented escape hatch _is_ a feature flag.

The 4.23 upgrade guide (_camel-xpath_, from CAMEL-24475) tells users that 
Camel's {{DocumentBuilderFactory}} "disallows a {{DOCTYPE}} declaration and 
does not resolve external entities", that such a document is "rejected with a 
{{SAXParseException}}", and that the way to parse one anyway is:

{code:none}
-Dorg.apache.camel.xmlconverter.documentBuilderFactory.feature:http://apache.org/xml/features/disallow-doctype-decl=false
{code}

Under the floor that property still relaxes the DOCTYPE check, but it no longer 
restores external entity resolution, because the fetch is blocked one layer 
below the flag. The escape hatch therefore has to be restated as either:

* install an {{EntityResolver}} on the {{DocumentBuilder}} that allow-lists 
what the deployment genuinely needs, or
* supply your own {{DocumentBuilderFactory}}, matching what SAX already says.

*Interaction with CAMEL-23878.* That issue asks for {{createSAXParserFactory}} 
to be aligned with {{createDocumentBuilderFactory}} by adding 
{{disallow-doctype-decl=true}} and {{external-parameter-entities=false}} to the 
SAX path, and notes that code relying on DOCTYPE handling through SAX 
conversion would break. With a resolver floor the cheaper convergence runs the 
other way: *let both paths accept a DOCTYPE and let the floor neutralise its 
external references*, rather than teaching SAX to reject documents it accepts 
today. That closes CAMEL-23878 by making the two paths agree, with no breakage 
on either side.

The counter-argument has to be recorded honestly: dropping 
{{disallow-doctype-decl}} from the DOM path relaxes a default that the 4.23 
upgrade guide promised in the same release, and the threat model's committer 
checklist requires an upgrade-guide entry plus PMC review for exactly that. It 
is a decision for the PMC, not an implementation detail. Measured behaviour to 
inform it: with the floor in place and the DOCTYPE permitted, a document 
carrying {{<!ENTITY xxe SYSTEM "file:///...">}} parses and the entity expands 
to *empty* on DOM, SAX and StAX alike; nothing is read.

Sites: {{XmlConverter.createDocumentBuilderFactory()}}, {{XMLConverterHelper}}, 
{{XmlLineNumberParser}}, {{BaseParser}}, {{RouteCoverageXmlParser}}, 
{{ProcessorDetailDevConsole}}, {{FlatpackConverter}}, 
{{CMSenderOneMessageImpl}}, {{SaxonXmlSourceHandlerFactoryImpl}} (currently 
unhardened).

h2. 3. StAX

Little work. {{XMLInputFactory.newInstance()}} -> 
{{SecureXMLInputFactory.newFactory()}}.

The properties currently set can be reclassified rather than simply deleted:

* {{SUPPORT_DTD}} and {{IS_SUPPORTING_EXTERNAL_ENTITIES}} become 
*optimisations*. The floor would catch these references anyway; switching them 
off stops the parser from walking a DTD it is not allowed to act on. Keeping 
them is cheap and shortens the path.
* {{IS_REPLACING_ENTITY_REFERENCES}} is *not a security property* at all. It 
selects whether entity references are reported as events or replaced inline, 
and should be documented as a parsing choice rather than left sitting in a 
hardening block.

{{StaxConverter}} also installs an {{XMLResolver}} that throws {{"Reading 
external entities is disabled"}}. Under the floor it becomes the delegate and 
still throws, which is how the loud failure mode is preserved against the 
library's silent-empty default. Worth keeping for that reason, though note 
{{SUPPORT_DTD=false}} pre-empts it for DOCTYPE-bearing payloads, so it is 
reached less often than it appears.

{{XMLOutputFactory}} is unaffected; it writes.

Sites: {{StaxConverter}}, {{XmlStreamDetector}}, {{UblDataFormat}}, 
{{StAXJAXBIteratorExpression}}.

h2. 4. Validation

Camel already has the concept this needs: the global option 
{{CamelXmlValidatorAccessExternalDTD}} ({{SchemaReader.ACCESS_EXTERNAL_DTD}}, 
CAMEL-10894), which today skips the {{ACCESS_EXTERNAL_DTD}} / 
{{ACCESS_EXTERNAL_SCHEMA}} properties on the {{SchemaFactory}} and the 
{{Validator}}. Re-express it as a resolver:

* {{SchemaFactory.newInstance(lang)}} -> 
{{SecureSchemaFactory.newInstance(lang)}}.
* When the option is off, install only the resolver the endpoint is configured 
with. The default {{DefaultLSResourceResolver}} already serves {{xs:import}} / 
{{xs:include}} relative to the endpoint's resource URI, and continues to do so 
as the floor's delegate, so multi-file schemas are unaffected.
* When the option is on, chain an allow-all {{LSResourceResolver}} _after_ the 
configured one, so a schema that resolves locally still does, and anything else 
is opted in.

Two refinements worth considering while the code is open:

* Split the single switch into {{CamelXmlValidatorAccessExternalDTD}} and a new 
{{CamelXmlValidatorAccessExternalSchema}}, mirroring the JAXP distinction 
between {{ACCESS_EXTERNAL_DTD}} and {{ACCESS_EXTERNAL_SCHEMA}}. Today one 
option controls both, which forces a deployment that needs a remote schema to 
also accept remote DTDs.
* The allow-all resolver must return an {{LSInput}} carrying *identifiers 
only*, with no stream attached, so the parser opens the resource itself. 
Opening it inside the resolver forces the checked {{IOException}} to be 
swallowed (the interface declares none), which converts a real fetch failure 
into silent empty content. This is not theoretical: it is what 
{{ValidatorDtdAccessOnTest}} detects, because it asserts on the 
{{UnknownHostException}} reaching the route.

{{ValidatingProcessor}} should keep setting {{ACCESS_EXTERNAL_*}} on the 
non-opt-in path: the {{Schema}} may come from a user-injected 
{{SchemaFactory}}, which Camel cannot secure, and that path would otherwise 
lose its only protection.

Sites: {{SchemaReader}}, {{ValidatingProcessor}}, {{XmlSignatureProcessor}}, 
{{JaxbDataFormat}} (see _Follow-ups_).

h2. 5. TrAX

The most wiring, because the protection works differently here.

Commons Secure XML secures a transform by *replacing every parser-less source 
with one it owns*: a {{StreamSource}}, or a {{SAXSource}} whose 
{{getXMLReader()}} returns null, is rewritten to carry a secure {{XMLReader}} 
before the implementation ever sees it. A {{DOMSource}}, or a {{SAXSource}} 
that already carries the caller's own reader, is passed through untouched, on 
the assumption that the caller has taken responsibility for it. The same 
rewrite is applied to sources handed back by a {{URIResolver}}, so an opted-in 
{{xsl:include}} is parsed on the same floor as everything else.

Camel can and should do the same thing for the sources it constructs itself, so 
that a body converted to a {{Source}} by the type converter arrives at the 
transform already carrying a secure reader rather than relying on the TrAX 
implementation to add one: {{XmlConverter.toSAXSource}} / {{toStreamSource}} 
and {{XsltBuilder}} are the places.

Notes:

* {{XsltUriResolver}} already serves {{xsl:include}}, {{xsl:import}} and 
{{document()}} over {{classpath:}}, {{file:}}, {{http:}}, {{ref:}} and 
{{bean:}}, and keeps working as the floor's delegate.
* A top-level URI the caller passes is fetched as-is by the implementation; 
only follow-up references go through the floor. Camel already resolves the 
top-level stylesheet itself through {{XsltUriResolver}} 
({{XsltEndpoint.loadResource}}), so the opt-in policy applied to the main 
stylesheet must be applied to its includes too. That is what the existing 
resolver does, and it is the property to protect in review.
* The guarantees govern what a transform reads, not what it writes. 
{{xsl:result-document}} still writes wherever the stylesheet says, so an 
untrusted stylesheet remains a route-author concern.
* {{XmlConverter.configureSaxonTransformerFactory}} and 
{{StaxConverter.isWoodstox}} both identify the implementation by 
{{factory.getClass().getName()}}, which becomes the wrapper. Both need 
restructuring: probe the classpath and let {{setAttribute}} reject a non-Saxon 
factory (attributes are delegated, so a real Saxon still gets configured), and 
resolve the StAX implementation once from a plain factory.

Sites: {{XmlConverter.createTransformerFactory()}}, {{XMLConverterHelper}}, 
{{XsltBuilder}}, {{CachedCxfPayload}}, {{SpringWebserviceHelper}}, 
{{TikaProducer}}, {{FopProducer}}, {{RouteCoverageMojo}}.

h1. Threat model

{{security-model.adoc}} must gain a list of the settings that *invalidate the 
guarantees*, alongside the existing XXE and SSRF sections. A guarantee that can 
be switched off silently is not a guarantee, and these are the switches:

|| Setting || Effect ||
| {{org.apache.camel.xmlconverter.documentBuilderFactory.feature:<uri>}} with 
{{external-general-entities}}, {{external-parameter-entities}} or 
{{load-external-dtd}} set to {{true}} | re-enables external entity resolution 
on the DOM path |
| {{org.apache.camel.xmlconverter.documentBuilderFactory.feature:<uri>}} with 
{{disallow-doctype-decl}} set to {{false}} | permits a DOCTYPE; external 
references stay blocked unless a resolver opts them in |
| {{CamelXmlValidatorAccessExternalDTD}} (and a future 
{{CamelXmlValidatorAccessExternalSchema}}) | allows the validator to fetch 
external DTDs and schemas |
| a caller-supplied {{DocumentBuilderFactory}}, {{TransformerFactory}} or 
{{SchemaFactory}} ({{XmlConverter.setDocumentBuilderFactory}}, 
{{validator:...?schemaFactory=#ref}}, the {{CamelDocumentBuilderFactory}} 
exchange property) | bypasses the floor entirely; the library exposes no way to 
wrap an existing instance |
| a caller-installed {{EntityResolver}} / {{URIResolver}} / 
{{LSResourceResolver}} / {{XMLResolver}} | opts in whatever it resolves; the 
floor only covers what the resolver declines |

The last two are the important ones, because they are the extension points the 
framework offers by design. The threat model should state plainly that a 
factory or resolver supplied by the deployment is the deployment's 
responsibility, which is consistent with operators and route authors being 
trusted, but is currently implicit.

h1. Dependency

{{org.apache.commons:commons-secure-xml:1.0.0}}, released to Maven Central in 
September 2026. Java 8 target, {{Multi-Release}} jar, OSGi bundle 
{{org.apache.commons.xml.secure}}, Apache 2.0. It has *no transitive compile 
dependencies*; Saxon-HE is {{provided}} and imported optionally. Managed in 
{{parent/pom.xml}} as {{commons-secure-xml-version}}; {{camel-dependencies}} 
picks the property up automatically and neither the BOM nor the catalog needs 
an edit.

This would be the first third-party compile dependency in 
{{core/camel-xml-jaxp}}. The whole {{core/}} tree currently carries four 
(slf4j-api, jspecify, JAXB, Jackson-YAML), so the addition needs justification 
on the record: it removes roughly 120 lines of security code whose divergence 
is the direct cause of four accepted CVEs, and replaces it with a guarantee 
that does not depend on which parser the deployment happens to ship.

h1. Follow-ups, deliberately out of scope

* *Saxon-selecting components*: {{camel-xslt-saxon}}, {{camel-xj}}, 
{{camel-schematron}} and {{camel-xpath}}'s Saxon path all promise a specific 
implementation class ({{transformerFactoryClass}}, {{saxon=true}}, 
{{objectModelUri}}) and then act on {{instanceof}} or a class-name check. A 
wrapper makes those checks fail *silently*, so {{saxonConfiguration}}, 
{{saxonExtensionFunctions}}, {{secureProcessing}} and Saxon's {{linenumbering}} 
attribute would stop being applied with no error. These stay on their current 
hardening until Commons Secure XML offers a public {{wrap(TransformerFactory)}} 
/ {{unwrap()}} pair, which is worth raising upstream as a feature request.
* *camel-jaxb* compiles multi-file schemas with no {{LSResourceResolver}}, 
relying on its {{accessExternalSchemaProtocols}} option. Securing it before 
giving it a resolver would turn "schema fails to load loudly" into "schema 
silently validates nothing", which is worse than the current behaviour. It 
needs the resolver first.
* *camel-jbang, camel-kamelet-main, tooling*: three copied private 
secure-factory helpers, all missing {{disallow-doctype-decl}} and all 
swallowing exceptions, parsing POMs fetched from remote repositories, POMs from 
arbitrary third-party jars, and MCP-client-supplied strings. The least-trusted 
input in the repository, and the cleanest win once the core pattern is settled.




--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to