This is an automated email from the ASF dual-hosted git repository. coheigea pushed a commit to branch coheigea/internal-parser in repository https://gitbox.apache.org/repos/asf/ws-xmlschema.git
commit 4b260c333d092ff531e21336d450d82949547563 Author: Colm O hEigeartaigh <[email protected]> AuthorDate: Fri Aug 28 09:08:24 2026 +0100 Harden internal parser against DTDs --- README.txt | 13 ++ THREAT-MODEL.md | 190 ++++++++++----------- .../org/apache/ws/commons/schema/XmlSchema.java | 7 + .../ws/commons/schema/XmlSchemaCollection.java | 54 ++++++ .../java/tests/InternalParserHardeningTest.java | 77 +++++++++ 5 files changed, 239 insertions(+), 102 deletions(-) diff --git a/README.txt b/README.txt index f69ffb67..d4e26e1c 100644 --- a/README.txt +++ b/README.txt @@ -55,6 +55,17 @@ adjust the per-document limits: including nested include/import/redefine document resolutions. The default is 512. + The internal parser used by XmlSchemaCollection.read(InputSource), + read(Reader), stream-backed read(Source), and recursive + xs:import/xs:include/xs:redefine reparses rejects DOCTYPE declarations + by default and disables external DTD and external entity resolution. + To accept schema documents that contain a DOCTYPE declaration, set: + + org.apache.ws.commons.schema.allowDTD + Set to true to allow DOCTYPE declarations. The default is false. + External DTD and external entity resolution remain disabled when this + property is true. + For example, set a limit with: -Dorg.apache.ws.commons.schema.walker.maxDecisionPoints=20000 @@ -63,6 +74,8 @@ For example, set a limit with: -Dorg.apache.ws.commons.schema.maxNestingDepth=256 + -Dorg.apache.ws.commons.schema.allowDTD=true + =================== Support =================== diff --git a/THREAT-MODEL.md b/THREAT-MODEL.md index 4fedfad5..4f242b88 100644 --- a/THREAT-MODEL.md +++ b/THREAT-MODEL.md @@ -34,11 +34,10 @@ PMC review. - **Status**: under maintainer review. - **Reporting**: vulnerabilities that fall under §8 (claimed - properties) should be reported per the Apache Security Team disclosure - channel (<https://www.apache.org/security/>); reports that fall under - §3 (out of scope) or §9 (properties not provided) will be closed by - XMLSchema triagers citing this document. The project does not ship - an in-repo `SECURITY.md` at assessment time *(inferred — §14 Q1)*. + properties) should be reported per `SECURITY.md` and the Apache + Security Team disclosure channel (<https://www.apache.org/security/>); + reports that fall under §3 (out of scope) or §9 (properties not + provided) will be closed by XMLSchema triagers citing this document. - **Provenance legend** — *(documented)* = drawn from in-repo docs / source comments / project website with citation; @@ -91,7 +90,7 @@ filesystem IO** when it follows `<xs:include>` / `<xs:import>` / | **`ExtensionRegistry` implementation** | trusted | Pluggable via system property `org.apache.ws.commons.schema.extension_registry` *(documented: `xmlschema-core/src/main/java/org/apache/ws/commons/schema/XmlSchemaCollection.java` line 361)*. | | **Producer of the schema bytes** (`Reader`, `InputStream`, `InputSource`, `Source`, `Document`, `Element`) | **variable** — see §6 trust table | The *only* attacker-controllable input position; in many embeddings the schema bytes come from a WSDL fetched off the wire. | | **Producer of imported / included schemas** (resolved by the `URIResolver`) | **variable** — typically as untrusted as the parent schema, but can be a *different* origin if the parent's `<xs:import schemaLocation="http://attacker/evil.xsd">` points elsewhere | Following an `xs:import` is a **second, possibly cross-origin, fetch**. This is the principal SSRF surface. | -| **JDK XML platform** (`DocumentBuilderFactory`, `TransformerFactory`, `SchemaFactory`) | trusted upstream | XMLSchema sets `FEATURE_SECURE_PROCESSING=true` on the factories it constructs but does **not** set `disallow-doctype-decl` or unset external-entity properties explicitly *(documented: `XmlSchemaCollection.java` line 713, `XmlSchema.java` line 886, `DomBuilderFromSax.java` line 81)*. | +| **JDK XML platform** (`DocumentBuilderFactory`, `TransformerFactory`, `SchemaFactory`) | trusted upstream | XMLSchema sets `FEATURE_SECURE_PROCESSING=true` on the factories it constructs. Its internal schema parser also rejects DOCTYPE declarations by default and disables external DTD and external entity resolution; `org.apache.ws.commons.schema.allowDTD=true` accepts DOCTYPE-bearing schema documents while keeping external resolution disabled. | ### Component-family table @@ -100,7 +99,7 @@ filesystem IO** when it follows `<xs:include>` / `<xs:import>` / | `xmlschema-core` object model — `XmlSchema*` types, `XmlSchemaCollection`, `SchemaBuilder` | `XmlSchemaCollection.read(InputSource, ...)` and overloads | **no** for in-memory model; **yes** when the URI resolver follows `xs:import`/`xs:include` | **yes** | | `xmlschema-core` URI resolver — `DefaultURIResolver`, `CollectionURIResolver`, `URIResolver` | `XmlSchemaCollection.setSchemaResolver()` | **yes** — by design, fetches imported schemas | **yes** | | `xmlschema-core` serializer — `XmlSchemaSerializer` | `XmlSchema.write(OutputStream)` | **no** | **yes** | -| `xmlschema-core` resource loader (internal) — `DocumentBuilderFactory` and `TransformerFactory` instances configured with `FEATURE_SECURE_PROCESSING=true` | invoked by `XmlSchemaCollection.read(InputSource, ...)` and `XmlSchemaSerializer` | **no** | **yes** | +| `xmlschema-core` resource loader (internal) — hardened `DocumentBuilderFactory` and `TransformerFactory` instances | invoked by `XmlSchemaCollection.read(InputSource, ...)`, recursive schema reparses, and `XmlSchema.write(...)` | **no** except where the URI resolver fetches imported schemas before parsing | **yes** | | `xmlschema-walker` visitor — `XmlSchemaWalker`, `XmlSchemaVisitor`, `XmlSchemaScope` | walked by a caller-supplied `XmlSchemaVisitor` | **no** | **yes** | | `xmlschema-walker` element validator — `XmlSchemaElementValidator` | walked at SAX-event time; consults the schema model | **no** (operates on caller-supplied SAX events) | **yes** | | `xmlschema-walker` document path finder — `XmlSchemaPathFinder` | walked at SAX-event time; matches events against the schema model | **no** (operates on caller-supplied SAX events) | **yes** | @@ -151,10 +150,10 @@ A finding is in-model only if it reaches a row marked **yes**. | # | Transition | Authentication | Authorization | | --- | --- | --- | --- | | B1 | Caller → `XmlSchemaCollection.read(InputSource | InputStream | Reader | URL | Document | Element)` | none — caller is trusted | none | -| B2 | `XmlSchemaCollection.read(InputSource, ...)` → JDK `DocumentBuilder` (with `FEATURE_SECURE_PROCESSING=true`) | none | none | +| B2 | `XmlSchemaCollection.read(InputSource, ...)` → hardened JDK `DocumentBuilder` | none | DOCTYPE rejected by default; external DTD/entity resolution disabled | | B3 | Schema parser → `URIResolver.resolveEntity(namespace, schemaLocation, baseUri)` | none | bundled `DefaultURIResolver` does **no host filtering**: it constructs `new URL(new URL(baseUri), schemaLocation)` and hands back an `InputSource` pointing at it | | B4 | Resolved `InputSource` → `XmlSchemaCollection.read(InputSource, ...)` (recursive) | none | none | -| B5 | `XmlSchemaSerializer.serializeSchema(...)` → JDK `TransformerFactory` (with `FEATURE_SECURE_PROCESSING=true`) | none | none | +| B5 | `XmlSchema.write(...)` → JDK `TransformerFactory` (with `FEATURE_SECURE_PROCESSING=true` and external DTD/stylesheet access disabled where supported) | none | none | | B6 | `XmlSchemaCollection` ctor → `System.getProperty("org.apache.ws.commons.schema.extension_registry")` → `Class.forName()` | none | trusts system properties to be operator-controlled | ### Reachability preconditions per family @@ -162,13 +161,13 @@ A finding is in-model only if it reaches a row marked **yes**. - **`xmlschema-core` parser** (`XmlSchemaCollection.read(InputSource)`, `.read(InputStream)`, `.read(Reader)`): in-model when the bytes are attacker-controllable. XMLSchema sets `FEATURE_SECURE_PROCESSING=true` - on its internal `DocumentBuilderFactory`, but does **not** explicitly - call `setFeature("http://apache.org/xml/features/disallow-doctype-decl", - true)` or `setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, - false)` *(documented: `XmlSchemaCollection.java` line 713)*. - `FEATURE_SECURE_PROCESSING` mitigates XML Schema 1.0's most expensive - expansion cases on Xerces but does not fully disable DTD processing - on every JDK / parser combination *(inferred — §14 Q6)*. + on its internal `DocumentBuilderFactory`, rejects DOCTYPE declarations + by default, disables external general entities, external parameter + entities, and external DTD loading, and installs a no-op SAX + `EntityResolver`. Setting + `org.apache.ws.commons.schema.allowDTD=true` accepts DOCTYPE-bearing + schema documents but keeps external DTD and external entity resolution + disabled. - **`xmlschema-core` URI resolver** (`DefaultURIResolver`): in-model for SSRF / cross-origin fetch when the input schema is attacker- controlled and contains an `xs:import schemaLocation="…"`. The @@ -201,9 +200,10 @@ A finding is in-model only if it reaches a row marked **yes**. - **JDK**: minimum Java 17 in the 2.3.0 release; earlier releases supported Java 7 *(documented: `RELEASE-NOTE.txt`)*. - **JDK XML platform**: assumes a conformant `DocumentBuilderFactory`, - `TransformerFactory`, and SAX parser. The behavior of - `FEATURE_SECURE_PROCESSING` depends on the provider in use; XMLSchema - does not bundle Xerces *(inferred — §14 Q6)*. + `TransformerFactory`, and SAX parser. XMLSchema applies explicit DTD and + external access controls to its internal schema parser and transformer, + while unsupported optional JAXP features are ignored so the remaining + controls still apply. - **Network**: the bundled `DefaultURIResolver` will issue HTTP / HTTPS / `file://` / `jar:` fetches via the JDK URL handlers when an `xs:include` / `xs:import schemaLocation` is followed. Whether the JVM has a proxy @@ -264,6 +264,7 @@ points*: | `org.apache.ws.commons.schema.maxImportDepth` system property | `64` *(documented: `README.txt`)* | operator-tunable per-process limit | maximum import/include resolution depth for one schema read | | `org.apache.ws.commons.schema.maxSchemaResolutions` system property | `1000` *(documented: `README.txt`)* | operator-tunable per-process limit | maximum schema documents resolved during one top-level read | | `org.apache.ws.commons.schema.maxNestingDepth` system property | `512` *(documented: `README.txt`)* | operator-tunable per-process limit | maximum structural nesting depth while building the schema model, including nested include/import/redefine document resolutions | +| `org.apache.ws.commons.schema.allowDTD` system property | `false` *(documented: `README.txt`)* | compatibility toggle for operator-controlled deployments | allows DOCTYPE declarations in schema documents; external DTD and external entity resolution remain disabled | | `DocumentBuilderFactory` provider | JDK default (typically Xerces fork) *(inferred — §14 Q6)* | depends on the JDK | shape of XML parsing for `read(InputSource)` / `read(InputStream)` paths | ### The insecure-default case @@ -276,15 +277,13 @@ should be protected by the default) or an `OUT-OF-MODEL: non-default-build` report (production deployments are *documented* as required to install a restricting resolver per §10). -XMLSchema sets `FEATURE_SECURE_PROCESSING=true` on its -`DocumentBuilderFactory` and `TransformerFactory` instances -*(documented: `XmlSchemaCollection.java` line 713, -`XmlSchema.java` line 886)* but does **not** call -`setFeature("http://apache.org/xml/features/disallow-doctype-decl", -true)`. The maintainer ruling on whether XXE / billion-laughs reports -against a callable `read(InputSource)` are `VALID` (the secure-processing -feature is the intended defense and is sufficient) or `MODEL-GAP` -(stricter feature toggles should be set) is captured in §14 Q6. +XMLSchema's internal schema parser rejects DOCTYPE declarations by +default and disables external DTD and external entity resolution. This +applies both to top-level `read(InputSource | InputStream | Reader)` +parses and to recursive import/include/redefine reparses. Operators may +set `org.apache.ws.commons.schema.allowDTD=true` to accept +DOCTYPE-bearing schemas for compatibility; external DTD and external +entity resolution remain disabled in that mode. ## §6 Assumptions about inputs @@ -292,7 +291,7 @@ feature is the intended defense and is sufficient) or `MODEL-GAP` | Entry point | Parameter | Attacker-controllable? | Caller must enforce | | --- | --- | --- | --- | -| `XmlSchemaCollection.read(InputSource is)` | `is` bytes | **yes** | nothing — XMLSchema sets `FEATURE_SECURE_PROCESSING=true` on its `DocumentBuilderFactory`; caller may need to install a restricting `URIResolver` if the source contains untrusted `xs:include`/`xs:import` | +| `XmlSchemaCollection.read(InputSource is)` | `is` bytes | **yes** | XMLSchema rejects DOCTYPE by default and disables external DTD/entity resolution; caller may need to install a restricting `URIResolver` if the source contains untrusted `xs:include`/`xs:import` | | `XmlSchemaCollection.read(InputStream in)` | `in` bytes | **yes** | same as above | | `XmlSchemaCollection.read(Reader r)` | `r` characters | **yes** | same as above | | `XmlSchemaCollection.read(URL url)` | `url` | caller-supplied | caller controls; XMLSchema fetches via JDK URL handlers | @@ -301,7 +300,7 @@ feature is the intended defense and is sufficient) or `MODEL-GAP` | `XmlSchemaCollection.setSchemaResolver(URIResolver)` | resolver | caller-supplied | replacing the default is the documented path for production hardening *(inferred — §14 Q12)* | | `XmlSchemaCollection.setBaseUri(String)` | `baseUri` | **caller-supplied trusted string** | not validated; if attacker can set this they can pivot the import-resolver origin | | `XmlSchemaCollection.setExtReg(ExtensionRegistry)` | registry | caller-supplied | caller's choice | -| `XmlSchema.write(OutputStream)` / `XmlSchema.write(Writer)` | output sink | caller-supplied | caller's choice; `FEATURE_SECURE_PROCESSING=true` is set on the internal `TransformerFactory` *(documented: `XmlSchema.java` line 886)* | +| `XmlSchema.write(OutputStream)` / `XmlSchema.write(Writer)` | output sink | caller-supplied | caller's choice; `FEATURE_SECURE_PROCESSING=true` is set on the internal `TransformerFactory`, with external DTD/stylesheet access disabled where supported | | `XmlSchemaWalker.walk(XmlSchemaElement)` | walked schema | as untrusted as the schema | none — pure in-memory walking | | `XmlSchemaPathFinder` | SAX events + schema model | as untrusted as both | configure backtracking limits for the deployment; defaults bound decision points and replayed events per document | | `XmlSchemaElementValidator` (walker module) | SAX events + schema model | as untrusted as both | caller validates / sanitizes outside | @@ -367,17 +366,23 @@ feature is the intended defense and is sufficient) or `MODEL-GAP` the documented surface *(inferred — §14 Q15)*. - *(inferred — §14 Q15)* -### P2 — Some mitigation of "expensive" schema expansion via `FEATURE_SECURE_PROCESSING=true` on the internal `DocumentBuilderFactory` and `TransformerFactory` - -- **Condition**: the JDK XML provider honors `FEATURE_SECURE_PROCESSING`; - XMLSchema's `read(InputSource | InputStream | Reader)` paths take the - internal `DocumentBuilderFactory`. -- **Violation symptom**: a billion-laughs / quadratic-expansion attack - parses to completion (no `SAXException`, no `XmlSchemaException`), - the host JVM runs out of memory or pegs the CPU. -- **Severity**: **maintainer ruling required** — see §14 Q6. -- *(documented: `XmlSchemaCollection.java` line 713, - `XmlSchema.java` line 886, `DomBuilderFromSax.java` line 81)* +### P2 — Internal parser DTD and external-entity hardening + +- **Condition**: XMLSchema's `read(InputSource | InputStream | Reader)` + paths take the internal `DocumentBuilderFactory`. +- **Property**: DOCTYPE declarations are rejected by default. External + general entities, external parameter entities, external DTD loading, + and JAXP external-DTD access are disabled; a no-op `EntityResolver` is + installed as a fallback. If + `org.apache.ws.commons.schema.allowDTD=true` is set, DOCTYPE + declarations may be accepted but external DTD/entity resolution remains + disabled. +- **Violation symptom**: attacker-controlled schema bytes cause the + internal parser to fetch or expand external DTD/entity content, or a + DOCTYPE is accepted while `allowDTD` is unset. +- **Severity**: **high** for external file disclosure / SSRF through XXE; + **medium** for parser-resource exhaustion when the embedding application + accepts untrusted schemas. ### P3 — Round-trip parse → model → serialize consistency @@ -437,14 +442,10 @@ matching disclaimer. host filtering of any kind. The caller is responsible for installing a restricting `URIResolver` if the input schema is attacker-controlled *(documented: `DefaultURIResolver.java`)*. -- **No XXE / DTD defense beyond `FEATURE_SECURE_PROCESSING=true`.** - XMLSchema does **not** call - `setFeature("http://apache.org/xml/features/disallow-doctype-decl", - true)`, does **not** set - `setFeature("http://xml.org/sax/features/external-general-entities", - false)`, and does **not** unset the analogous `XMLInputFactory` - properties. Whether this is sufficient depends on the JDK XML - provider in use *(inferred — §14 Q6)*. +- **No guarantee that DTD-bearing schemas are accepted by default.** + XMLSchema rejects DOCTYPE declarations on its internal parser unless + `org.apache.ws.commons.schema.allowDTD=true` is set. The compatibility + mode still disables external DTD and external entity resolution. - **No defense when the caller passes in a pre-parsed `Document` or `Element`.** The hardening on the internal `DocumentBuilderFactory` is moot — the caller's parser produced the DOM *(documented: @@ -471,16 +472,10 @@ matching disclaimer. ### False-friend properties (call out separately) -- **`FEATURE_SECURE_PROCESSING=true` looks like full XXE defense, - but it is not.** On many JDK XML providers it imposes safe limits on - entity expansion (XML-1.0 quadratic-blow-up bounds) but does **not** - disable external entities or DTD processing entirely. Strict XXE - defense in Java requires the explicit - `setFeature("http://apache.org/xml/features/disallow-doctype-decl", - true)` plus `setFeature("http://xml.org/sax/features/external-general-entities", - false)` and `setFeature("http://xml.org/sax/features/external-parameter-entities", - false)`. XMLSchema does the *FEATURE_SECURE_PROCESSING* hardening - but not the explicit XXE flags *(inferred — §14 Q6)*. +- **`org.apache.ws.commons.schema.allowDTD=true` looks like it restores + legacy XML parser behavior, but it does not restore external DTD or + external entity fetching.** The toggle accepts DOCTYPE declarations for + compatibility only; external fetches remain disabled. - **`DefaultURIResolver` looks like a sandbox, but it isn't.** It is the *bundled* resolver; its job is to resolve `xs:include`/`xs:import`, not to filter destinations. @@ -495,9 +490,10 @@ matching disclaimer. ### Well-known attack classes XMLSchema does not single-handedly defend against -- **XXE / external-entity disclosure** via DTD-enabled JDK XML - providers — depends on the JDK XML factory and on caller-side - hardening when `read(Document)` / `read(Element)` is used. +- **XXE / external-entity disclosure** when the caller pre-parses a DOM + with an unsafe XML parser before calling `read(Document)` / + `read(Element)`. XMLSchema's internal parser path rejects DOCTYPE by + default and disables external DTD/entity resolution. - **SSRF via `xs:import schemaLocation`** — see §9 first bullet. - **Billion-laughs / quadratic blowup** — partially mitigated by `FEATURE_SECURE_PROCESSING=true`, but not universally. @@ -522,10 +518,10 @@ The embedding Java application **must**: `XmlSchemaCollection.read(...)`, use a `DocumentBuilderFactory` hardened against XXE — specifically with `disallow-doctype-decl=true` and external-entity processing disabled. -3. When using the `read(InputSource | InputStream | Reader)` path - against attacker-controlled bytes, verify the JDK XML provider in - use treats `FEATURE_SECURE_PROCESSING=true` as sufficient defense - *(inferred — §14 Q6)*. +3. Do not enable `org.apache.ws.commons.schema.allowDTD=true` for + attacker-controlled schema bytes unless compatibility requires it and + the deployment accepts internal DTD subset processing. External DTD and + external entity resolution remain disabled by XMLSchema in that mode. 4. Bound maximum schema size, imported bytes, and fetch rate at the *caller* level. XMLSchema provides configurable import/include depth per-read resolution, and structural nesting limits, but these do not @@ -544,10 +540,9 @@ defense-in-depth controls: 1. Enforce URL scheme and destination restrictions in the resolver: allow only `https://` to approved hosts; deny `file://`, `jar:`, loopback, link-local, and RFC1918/private address ranges. -2. Set explicit XML parser anti-XXE features in addition to - `FEATURE_SECURE_PROCESSING=true`: `disallow-doctype-decl=true`, - `external-general-entities=false`, and - `external-parameter-entities=false`. +2. Keep `org.apache.ws.commons.schema.allowDTD` unset unless a deployment + has known DTD-bearing schema inputs and has tested the compatibility + mode on its JDK XML provider. 3. Supplement XMLSchema's import/include depth, per-read resolution, and structural nesting limits with caller-boundary budgets for total imported bytes and fetch rate per top-level parse. @@ -599,11 +594,10 @@ model, the section that licenses the call. per §10 item 1. → `OUT-OF-MODEL: trusted-input` *(if the maintainer rules at Q12 that the default is dev/test)*, or `VALID-HARDENING` *(if the maintainer rules the default is supported)*. -- **"`DocumentBuilderFactory.newInstance()` not setting - `disallow-doctype-decl`."** Investigated under §14 Q6 — XMLSchema - *does* set `FEATURE_SECURE_PROCESSING=true` which is the - XSLT-conventional hardening lever. → `KNOWN-NON-FINDING` if the Q6 - ruling lands "secure-processing is sufficient", else `VALID-HARDENING`. +- **"`DocumentBuilderFactory.newInstance()` allows XXE in + `XmlSchemaCollection`."** Current XMLSchema internal parsing rejects + DOCTYPE by default and disables external DTD/entity resolution. A report + must show a bypass of those controls to be `VALID`. - **"`Class.forName(System.getProperty(...))` is dynamic-class-loading."** Documented extension point; the system property is the trust gate *(documented: `XmlSchemaCollection.java` line 361)*. → `OUT-OF-MODEL: @@ -617,7 +611,7 @@ model, the section that licenses the call. `OUT-OF-MODEL: unsupported-component`. - **"`XmlSchemaSerializer` uses identity Transformer — could be exploited via XSLT injection."** Identity transform; no stylesheet - reachable from input *(documented: `XmlSchema.java` line 886)*. → + reachable from input *(documented: `XmlSchema.java`)*. → `KNOWN-NON-FINDING`. - **"`URLConnection.getInputStream()` without timeout."** True; XMLSchema does no read-timeout on fetched imports @@ -641,8 +635,8 @@ Revise this document when any of the following lands: - A change in the default `URIResolver` behavior — e.g. adding host filtering, refusing non-HTTPS, or adding read/connect timeouts. -- A change in the default `FEATURE_SECURE_PROCESSING=true` hardening — - e.g. adding explicit `disallow-doctype-decl=true`. +- A change in the default parser DTD/XXE posture, including the + `org.apache.ws.commons.schema.allowDTD` compatibility contract. - A new public entry point on `XmlSchemaCollection` that accepts new input shapes. - A new built-in resource limit, or a change to an existing resource @@ -668,7 +662,7 @@ A report against XMLSchema receives exactly one of the following: | `OUT-OF-MODEL: unsupported-component` | Lands in `w3c-testcases/`, `*/src/test/`, `etc/`, `xmlschema-bundle-test/`. | §3 items 4, 8 | | `OUT-OF-MODEL: non-default-build` | Only manifests under a §5a configuration the maintainer rules dev/test (e.g. an unsafe custom `URIResolver`). | §5a | | `OUT-OF-MODEL: out-of-layer` | Concerns a *document* validation step delegated to `javax.xml.validation.Validator`, or a WSDL parser upstream. | §3 items 1–3 | -| `BY-DESIGN: property-disclaimed` | Concerns a §9 property the project explicitly does not provide (no SSRF defense, no XXE defense beyond secure-processing, no schema-size, imported-byte, or fetch-rate ceiling). | §9 | +| `BY-DESIGN: property-disclaimed` | Concerns a §9 property the project explicitly does not provide (no SSRF defense, no guarantee of default DTD acceptance, no schema-size, imported-byte, or fetch-rate ceiling). | §9 | | `KNOWN-NON-FINDING` | Matches a §11a recurring false positive. | §11a | | `MODEL-GAP` | Cannot be cleanly routed to any of the above — triggers §12 model revision. | §12 | @@ -702,27 +696,17 @@ makes no XXE claim about it" (`OUT-OF-MODEL: trusted-input`). Confirm? **Q5.** When the URI resolver follows an `xs:import schemaLocation`, proposed position is "what the remote host returns is parsed by the -*caller-installed* parser path; the *fetch* itself is the +internal XMLSchema parser path; the *fetch* itself is the attacker-influenced action and §9 disclaims SSRF defense" (`§9`). Confirm? *(maps to §3 item 6, §9)* -**Q6.** **The big XXE question.** XMLSchema sets -`FEATURE_SECURE_PROCESSING=true` on its internal -`DocumentBuilderFactory` and `TransformerFactory`. It does **not** set -`disallow-doctype-decl=true`, `external-general-entities=false`, or -`external-parameter-entities=false`. The maintainer ruling is required -on: - -- (a) Is `FEATURE_SECURE_PROCESSING=true` considered the sufficient - defense (so XXE reports against - `XmlSchemaCollection.read(InputSource)` are `KNOWN-NON-FINDING`)? -- (b) Or are explicit `disallow-doctype-decl=true` and external-entity - toggles the supported posture, with the current state being a - `VALID-HARDENING` to be addressed? - -Proposed answer: **(b), with a future-PR to add the explicit toggles -and document `FEATURE_SECURE_PROCESSING=true` as the present -inheritance from JDK Xerces.** *(maps to §5a, §8 P2, §9, §11a)* +**Q6.** **DTD compatibility posture.** XMLSchema now rejects DOCTYPE +declarations by default on the internal schema parser, disables external +DTD/entity resolution, and offers +`org.apache.ws.commons.schema.allowDTD=true` to accept DTD-bearing schema +documents while keeping external resolution disabled. Confirm that this is +the supported posture for untrusted schema bytes. *(maps to §5a, §8 P2, +§9, §10, §11a)* ### Wave 3 — URI resolver / SSRF @@ -826,20 +810,22 @@ to §4, §6, §8 P5)* ## Appendix: SECURITY.md / website → §x back-map -XMLSchema does not ship a `SECURITY.md`. The threat-model sources are -the in-repo `README.txt`, the `RELEASE-NOTE.txt`, and the JavaDoc / -source comments. The project website is +XMLSchema ships a `SECURITY.md` that points vulnerability reporters to +the Apache Software Foundation security process. The threat-model sources +are the in-repo `README.txt`, the `RELEASE-NOTE.txt`, `SECURITY.md`, and +the JavaDoc / source comments. The project website is <https://ws.apache.org/xmlschema/>. | Source | Claim | Lands in | | --- | --- | --- | +| `SECURITY.md` | vulnerability reports go through the Apache Software Foundation security process and this threat model documents scope / triage | §1 | | `README.txt` | "lightweight schema object model that can be used to manipulate and generate XML schema representations" | §1, §2 intended use | | `RELEASE-NOTE.txt` (2.3.0) | Java 17 minimum, Java 7 dropped | §5 environment | | `xmlschema-core/src/main/java/org/apache/ws/commons/schema/XmlSchemaCollection.java` line 361 | `org.apache.ws.commons.schema.extension_registry` system property loaded via `Class.forName` | §5a, §6, §11 | | `xmlschema-core/src/main/java/org/apache/ws/commons/schema/SchemaBuilder.java` | `org.apache.ws.commons.schema.maxNestingDepth` structural descent limit | §5, §5a, §6, §8 P6 | -| `XmlSchemaCollection.java` line 713 | `docFac.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, TRUE)` | §5a, §8 P2 | +| `XmlSchemaCollection.java` | internal parser sets `FEATURE_SECURE_PROCESSING`, rejects DOCTYPE by default, disables external DTD/entity resolution, and honors `org.apache.ws.commons.schema.allowDTD` for compatibility | §5a, §8 P2 | | `XmlSchemaCollection.java` line 745 | `AccessController.doPrivileged` wrapper for the SAX parse | §5 | -| `XmlSchema.java` line 886 | `trFac.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, TRUE)` for serializer | §5a, §8 P2 | +| `XmlSchema.java` | serializer `TransformerFactory` sets `FEATURE_SECURE_PROCESSING` and disables external DTD/stylesheet access where supported | §5a, §8 P2 | | `xmlschema-core/src/main/java/.../resolver/DefaultURIResolver.java` | URL composed from `baseUri` + `schemaLocation`; no filtering | §3 item 7, §9 SSRF disclaim, §10 item 1, §11 first bullet | | `xmlschema-core/src/main/java/.../resolver/URIResolver.java` | Resolver interface — caller-pluggable | §2 caller-roles, §10 item 1 | | `xmlschema-walker/src/main/java/.../docpath/DomBuilderFromSax.java` line 81 | `factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, TRUE)` | §5a, §8 P2 | diff --git a/xmlschema-core/src/main/java/org/apache/ws/commons/schema/XmlSchema.java b/xmlschema-core/src/main/java/org/apache/ws/commons/schema/XmlSchema.java index 45c7aa5e..0dec7703 100644 --- a/xmlschema-core/src/main/java/org/apache/ws/commons/schema/XmlSchema.java +++ b/xmlschema-core/src/main/java/org/apache/ws/commons/schema/XmlSchema.java @@ -885,6 +885,13 @@ public class XmlSchema extends XmlSchemaAnnotated implements NamespaceContextOwn TransformerFactory trFac = TransformerFactory.newInstance(); trFac.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, Boolean.TRUE); + try { + trFac.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); + trFac.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, ""); + } catch (IllegalArgumentException e) { + // The provider does not recognize the JAXP 1.5 attributes. + } + try { trFac.setAttribute("indent-number", "4"); } catch (IllegalArgumentException e) { diff --git a/xmlschema-core/src/main/java/org/apache/ws/commons/schema/XmlSchemaCollection.java b/xmlschema-core/src/main/java/org/apache/ws/commons/schema/XmlSchemaCollection.java index 1383ee30..d20c08bb 100644 --- a/xmlschema-core/src/main/java/org/apache/ws/commons/schema/XmlSchemaCollection.java +++ b/xmlschema-core/src/main/java/org/apache/ws/commons/schema/XmlSchemaCollection.java @@ -21,6 +21,7 @@ package org.apache.ws.commons.schema; import java.io.IOException; import java.io.Reader; +import java.io.StringReader; import java.math.BigInteger; import java.security.AccessController; import java.security.PrivilegedAction; @@ -50,6 +51,7 @@ import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; +import org.xml.sax.EntityResolver; import org.xml.sax.InputSource; import org.xml.sax.SAXException; @@ -71,6 +73,16 @@ public final class XmlSchemaCollection { */ String baseUri; + /** + * Prevents external DTD and entity content from being fetched by the + * internal parser, even if a JAXP provider ignores hardening features. + */ + private static final EntityResolver NO_OP_ENTITY_RESOLVER = new EntityResolver() { + public InputSource resolveEntity(String publicId, String systemId) { + return new InputSource(new StringReader("")); + } + }; + /** * Key that identifies a schema in a collection, composed of a targetNamespace and a system ID. */ @@ -760,7 +772,9 @@ public final class XmlSchemaCollection { DocumentBuilderFactory docFac = DocumentBuilderFactory.newInstance(); docFac.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, Boolean.TRUE); docFac.setNamespaceAware(true); + hardenAgainstDtdProcessing(docFac); final DocumentBuilder builder = docFac.newDocumentBuilder(); + builder.setEntityResolver(NO_OP_ENTITY_RESOLVER); Document doc = null; doc = parseDoPriv(inputSource, builder, doc); return read(doc, inputSource.getSystemId(), namespaceValidator); @@ -809,6 +823,46 @@ public final class XmlSchemaCollection { } return doc; } + + /** + * DTD-bearing schema documents are legal XML, but DTD processing is not + * required to build schema components and is unsafe for untrusted input. + */ + private static void hardenAgainstDtdProcessing(DocumentBuilderFactory docFac) { + if (!isDtdAllowed()) { + trySetFeature(docFac, "http://apache.org/xml/features/disallow-doctype-decl", true); + } + trySetFeature(docFac, "http://xml.org/sax/features/external-general-entities", false); + trySetFeature(docFac, "http://xml.org/sax/features/external-parameter-entities", false); + trySetFeature(docFac, "http://apache.org/xml/features/nonvalidating/load-external-dtd", false); + try { + docFac.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); + } catch (IllegalArgumentException e) { + // The provider does not recognize the JAXP 1.5 attribute. + } + docFac.setExpandEntityReferences(false); + } + + private static void trySetFeature(DocumentBuilderFactory docFac, String feature, boolean value) { + try { + docFac.setFeature(feature, value); + } catch (ParserConfigurationException e) { + // The provider does not support this feature; the remaining + // hardening settings still apply. + } + } + + private static boolean isDtdAllowed() { + try { + return Boolean.parseBoolean(AccessController.doPrivileged(new PrivilegedAction<String>() { + public String run() { + return System.getProperty("org.apache.ws.commons.schema.allowDTD"); + } + })); + } catch (RuntimeException e) { + return false; + } + } /** * Find a global attribute by QName in this collection of schemas. diff --git a/xmlschema-core/src/test/java/tests/InternalParserHardeningTest.java b/xmlschema-core/src/test/java/tests/InternalParserHardeningTest.java new file mode 100644 index 00000000..37a23c8a --- /dev/null +++ b/xmlschema-core/src/test/java/tests/InternalParserHardeningTest.java @@ -0,0 +1,77 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package tests; + +import java.io.StringReader; + +import org.apache.ws.commons.schema.XmlSchema; +import org.apache.ws.commons.schema.XmlSchemaCollection; +import org.apache.ws.commons.schema.XmlSchemaException; +import org.apache.ws.commons.schema.resolver.URIResolver; + +import org.junit.Assert; +import org.junit.Test; +import org.xml.sax.InputSource; + +public class InternalParserHardeningTest extends Assert { + + private static final String PLAIN_SCHEMA = + "<xs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\"" + + " targetNamespace=\"urn:hardening\">" + + "<xs:element name=\"e\" type=\"xs:string\"/>" + + "</xs:schema>"; + + private static final String IMPORTING_SCHEMA = + "<xs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\"" + + " targetNamespace=\"urn:importer\">" + + "<xs:import namespace=\"urn:hardening\" schemaLocation=\"hostile.xsd\"/>" + + "</xs:schema>"; + + private static final String DOCTYPE_SCHEMA = + "<!DOCTYPE xs:schema [<!ENTITY payload \"expanded\">]>" + PLAIN_SCHEMA; + + @Test + public void testSchemaWithoutDoctypeStillParses() { + XmlSchemaCollection collection = new XmlSchemaCollection(); + XmlSchema schema = collection.read(new StringReader(PLAIN_SCHEMA)); + assertNotNull(schema); + assertEquals("urn:hardening", schema.getTargetNamespace()); + } + + @Test(expected = XmlSchemaException.class) + public void testSchemaWithDoctypeIsRejected() { + XmlSchemaCollection collection = new XmlSchemaCollection(); + collection.read(new StringReader(DOCTYPE_SCHEMA)); + } + + @Test(expected = XmlSchemaException.class) + public void testImportedSchemaWithDoctypeIsRejected() { + XmlSchemaCollection collection = new XmlSchemaCollection(); + collection.setSchemaResolver(new URIResolver() { + public InputSource resolveEntity(String targetNamespace, String schemaLocation, + String baseUri) { + InputSource source = new InputSource(new StringReader(DOCTYPE_SCHEMA)); + source.setSystemId(schemaLocation); + return source; + } + }); + collection.read(new StringReader(IMPORTING_SCHEMA)); + } +} \ No newline at end of file
