This is an automated email from the ASF dual-hosted git repository. coheigea pushed a commit to branch coheigea/file in repository https://gitbox.apache.org/repos/asf/ws-xmlschema.git
commit 54250822a9cb9ca4af65c9f880d515ef8064dd1f Author: Colm O hEigeartaigh <[email protected]> AuthorDate: Thu Sep 17 13:23:58 2026 +0100 Add a new property to disable reading local files --- README.txt | 22 +++++- THREAT-MODEL.md | 26 +++++-- .../schema/resolver/DefaultURIResolver.java | 31 +++++++- .../test/java/tests/DefaultURIResolverTest.java | 83 ++++++++++++++++++++++ 4 files changed, 154 insertions(+), 8 deletions(-) diff --git a/README.txt b/README.txt index 28643daa..1e5f2019 100644 --- a/README.txt +++ b/README.txt @@ -101,6 +101,18 @@ adjust the per-document limits: are unaffected either way, so this is not on its own a defence against an untrusted schema document - see the Security section below. + org.apache.ws.commons.schema.local.allowFileSystem + Whether a schema location may be read from the filesystem at all. The + default is true. Set it to false where schema documents are expected + to stand alone: a file: location, a jar:file: one, and a relative path + with no base URI to resolve it against are then refused rather than + read. Only true and false are recognised. + + Set alongside remote.allowNetwork=false, this leaves the bundled + resolver with nothing it will fetch, which is the closest it comes to + refusing every external reference. An application that must allow some + references and refuse others still needs its own URIResolver. + file: and jar: locations are read as before, without buffering. The collections returned by the "read-only" accessors on the schema model @@ -139,8 +151,14 @@ For example, set a limit with: Where a deployment needs no remote schemas at all, setting org.apache.ws.commons.schema.remote.allowNetwork to false refuses http and https locations outright, which closes the remote-fetch half of this - without any code. It does not restrict local file: reads, so it does not - replace a restricting resolver for untrusted input. + without any code. Where schema documents are expected to stand alone, + org.apache.ws.commons.schema.local.allowFileSystem=false closes the local + half as well, and the two together leave the bundled resolver with nothing + it will fetch. + + Neither switch replaces a restricting resolver for an application that has + to allow some references and refuse others: they are all-or-nothing per + transport, and they are read when the resolver is constructed. Applications that parse schema or WSDL documents from an untrusted source must install a restricting resolver before reading them: diff --git a/THREAT-MODEL.md b/THREAT-MODEL.md index 2cb0be48..639d0fca 100644 --- a/THREAT-MODEL.md +++ b/THREAT-MODEL.md @@ -286,6 +286,7 @@ points*: | `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.remote.allowNetwork` system property | `true` *(documented: `README.txt`)* | operator opt-out for deployments with no remote schema sets | when `false`, `DefaultURIResolver` refuses a location whose effective scheme is `http` or `https`; local `file:` / `jar:` reads are unaffected, so it closes the remote-fetch half of §9's SSRF disclaimer but not the local-read half | +| `org.apache.ws.commons.schema.local.allowFileSystem` system property | `true` *(documented: `README.txt`)* | operator opt-out for deployments whose schema documents stand alone | when `false`, `DefaultURIResolver` refuses a `file:` location, a `jar:file:` one, and a relative location with no base URI; with `remote.allowNetwork=false` it leaves the resolver with nothing to fetch, which is the nearest the shipped resolver comes to the catalog-only default §14 Q12(b) declined to make the [...] | `org.apache.ws.commons.schema.remote.connectTimeoutMillis` / `.readTimeoutMillis` / `.maxFetchMillis` / `.maxBytes` system properties | `5000` / `10000` / `30000` / `67108864` *(documented: `README.txt`)* | operator-tunable per-fetch bounds | bound one remote `DefaultURIResolver` fetch in wall-clock time and bytes; without them the JDK opens a `schemaLocation` with no timeout and no size limit, and a single import can hold a thread or its heap indefinitely | | `org.apache.ws.commons.schema.protectReadOnlyCollections` system property | `false` *(documented: `README.txt`, `CollectionFactory.java` lines 37-48)* | in-process convenience, not a trust boundary | when false, the "read-only" model accessors return the **live internal collections**, not unmodifiable views; §7 places the in-process caller outside the attacker model, so this is a correctness guard rather than a security control | | `DocumentBuilderFactory` provider | JDK default (typically Xerces fork) *(inferred — §14 Q6)* | depends on the JDK | shape of XML parsing for `read(InputSource)` / stream-shaped `read(Source)` paths | @@ -486,8 +487,12 @@ matching disclaimer. ratified — §14 Q12)*. An operator with no remote schema sets can set `org.apache.ws.commons.schema.remote.allowNetwork=false` to refuse `http` and `https` locations outright (§5a); that removes the SSRF - reach but not the local `file:` read, so it narrows this disclaimer - rather than retiring it. + reach but not the local `file:` read. Setting + `org.apache.ws.commons.schema.local.allowFileSystem=false` removes that + too, and the pair leaves the bundled resolver with nothing it will + fetch. Both are operator opt-outs read when the resolver is + constructed, and neither is the shipped default, so this disclaimer + still describes what a consumer gets out of the box. - **No guarantee that external DTD or external entity content is ever resolved.** XMLSchema accepts a DOCTYPE declaration, but never fetches an external DTD subset or an external entity; a schema that depends on @@ -567,8 +572,12 @@ The embedding Java application **must**: that throws rejects the read outright. A deployment that simply never needs a remote schema can instead set `org.apache.ws.commons.schema.remote.allowNetwork=false` (§5a), which - needs no code but still leaves local `file:` reads open, so it is not - a substitute for a restricting resolver on untrusted input. + needs no code but still leaves local `file:` reads open. A deployment + whose schema documents stand alone can add + `org.apache.ws.commons.schema.local.allowFileSystem=false` and be left + with a resolver that fetches nothing. Neither switch is a substitute + for a restricting resolver where some references must be allowed and + others refused: they are all-or-nothing per transport. 2. When passing a pre-parsed `Document` / `Element` into `XmlSchemaCollection.read(...)`, use a `DocumentBuilderFactory` hardened against XXE — specifically with `disallow-doctype-decl=true` @@ -740,6 +749,15 @@ Revise this document when any of the following lands: rule as first written: it tested only the URI authority, so `file:////host/share/x.xsd`, which parses with no authority and carries the host in its path instead, was not caught. +- **2026-09-17** — a companion + `org.apache.ws.commons.schema.local.allowFileSystem` system property + refuses `file:` and `jar:file:` locations, and a relative location with + no base URI, again defaulting to `true`. Recorded in §5a, §9 and §10 + item 1. Set with `remote.allowNetwork=false` it leaves the bundled + resolver with nothing to fetch, which is the nearest thing to the + catalog-only default §14 Q12(b) declined — but as an operator opt-out, + not a change of default, so the ruling and this model's disposition for + a report against the shipped default are unchanged. - **2026-09-17** — a new `org.apache.ws.commons.schema.remote.allowNetwork` system property lets an operator refuse `http` and `https` schema locations outright, defaulting to `true` so nothing changes for an diff --git a/xmlschema-core/src/main/java/org/apache/ws/commons/schema/resolver/DefaultURIResolver.java b/xmlschema-core/src/main/java/org/apache/ws/commons/schema/resolver/DefaultURIResolver.java index ff8d6b83..079235c0 100644 --- a/xmlschema-core/src/main/java/org/apache/ws/commons/schema/resolver/DefaultURIResolver.java +++ b/xmlschema-core/src/main/java/org/apache/ws/commons/schema/resolver/DefaultURIResolver.java @@ -46,8 +46,9 @@ import org.xml.sax.InputSource; * <code>jar</code>, and refuses a schema location that changes the scheme of a remote base URI, * names a non-local authority with the <code>file:</code> scheme, or reads a <code>jar:</code> * archive fetched over the network. A deployment with no remote schema sets can turn network - * resolution off altogether with the {@link #ALLOW_NETWORK_PROPERTY} system property, without - * supplying its own resolver. Within the schemes it does allow it + * resolution off altogether with the {@link #ALLOW_NETWORK_PROPERTY} system property, and + * filesystem resolution with {@link #ALLOW_FILE_SYSTEM_PROPERTY}, without supplying its own + * resolver. Within the schemes it does allow it * applies no host or address filtering, so any reachable host or readable file a schema location * names is fetched. An application that parses untrusted schema documents must install a restricting * resolver instead; see @@ -92,6 +93,20 @@ public class DefaultURIResolver implements CollectionURIResolver { public static final String ALLOW_NETWORK_PROPERTY = "org.apache.ws.commons.schema.remote.allowNetwork"; + /** + * Whether a schema location may be read from the filesystem at all. Set it to + * <code>false</code> in a deployment whose schema documents are expected to stand alone: an + * <code>xs:import</code> naming a <code>file:</code> location, a <code>jar:file:</code> one, + * or a relative path with no base URI to resolve it against, is then refused instead of read. + * It defaults to <code>true</code>. Setting it alongside + * {@link #ALLOW_NETWORK_PROPERTY} leaves this resolver with nothing it will fetch, which is + * the closest the bundled resolver comes to refusing every external reference; an + * application that needs to allow some references and refuse others still wants its own + * {@link URIResolver}. Only "true" and "false" are recognised. + */ + public static final String ALLOW_FILE_SYSTEM_PROPERTY = + "org.apache.ws.commons.schema.local.allowFileSystem"; + private static final long DEFAULT_CONNECT_TIMEOUT_MILLIS = 5L * 1000L; private static final long DEFAULT_READ_TIMEOUT_MILLIS = 10L * 1000L; private static final long DEFAULT_MAX_FETCH_MILLIS = 30L * 1000L; @@ -105,6 +120,7 @@ public class DefaultURIResolver implements CollectionURIResolver { getLongProperty(MAX_FETCH_MILLIS_PROPERTY, DEFAULT_MAX_FETCH_MILLIS); private final long maxBytes = getLongProperty(MAX_BYTES_PROPERTY, DEFAULT_MAX_BYTES); private final boolean allowNetwork = getBooleanProperty(ALLOW_NETWORK_PROPERTY, true); + private final boolean allowFileSystem = getBooleanProperty(ALLOW_FILE_SYSTEM_PROPERTY, true); private String collectionBaseURI; @@ -169,6 +185,12 @@ public class DefaultURIResolver implements CollectionURIResolver { } } if (isPlainRelativePath(schemaLocation)) { + if (!allowFileSystem) { + throw new XmlSchemaException("The schema location \"" + schemaLocation + + "\" is relative and would be resolved against the" + + " working directory, which " + + ALLOW_FILE_SYSTEM_PROPERTY + " has turned off."); + } return new InputSource(schemaLocation); } return null; @@ -397,6 +419,11 @@ public class DefaultURIResolver implements CollectionURIResolver { + "\" would be fetched over the network, which " + ALLOW_NETWORK_PROPERTY + " has turned off."); } + if (!allowFileSystem && "file".equals(scheme)) { + throw new XmlSchemaException("The schema location \"" + schemaLocation + + "\" would be read from the filesystem, which " + + ALLOW_FILE_SYSTEM_PROPERTY + " has turned off."); + } if ("file".equals(scheme) && !isLocalFileUri(archive)) { throw new XmlSchemaException("The schema location \"" + schemaLocation + "\" resolves to a file URL with a non-local authority."); diff --git a/xmlschema-core/src/test/java/tests/DefaultURIResolverTest.java b/xmlschema-core/src/test/java/tests/DefaultURIResolverTest.java index a07f991b..c9d8f5a1 100644 --- a/xmlschema-core/src/test/java/tests/DefaultURIResolverTest.java +++ b/xmlschema-core/src/test/java/tests/DefaultURIResolverTest.java @@ -353,4 +353,87 @@ public class DefaultURIResolverTest extends Assert { System.clearProperty(DefaultURIResolver.ALLOW_NETWORK_PROPERTY); } } + + @Test + public void testFileSystemResolutionCanBeTurnedOff() { + System.setProperty(DefaultURIResolver.ALLOW_FILE_SYSTEM_PROPERTY, "false"); + try { + DefaultURIResolver resolver = new DefaultURIResolver(); + assertRefusedWith(resolver, "file:///legit/local.xsd", null, "turned off"); + assertRefusedWith(resolver, "file:///legit/local.xsd", localBase(), "turned off"); + // jar:file: is a filesystem read too; effectiveScheme sees through the wrapper. + assertRefusedWith(resolver, "jar:file:///a.jar!/x.xsd", null, "turned off"); + // With no base a relative path is resolved against the working directory. + assertRefusedWith(resolver, "sub/x.xsd", null, "turned off"); + // The network switch is independent: this one does not touch http. + assertEquals("http://example.com/x.xsd", + resolver.resolveEntity("urn:x", "http://example.com/x.xsd", null) + .getSystemId()); + } finally { + System.clearProperty(DefaultURIResolver.ALLOW_FILE_SYSTEM_PROPERTY); + } + } + + /** Both switches off is the closest the bundled resolver comes to refusing everything. */ + @Test + public void testBothSwitchesOffLeaveNothingToFetch() { + System.setProperty(DefaultURIResolver.ALLOW_FILE_SYSTEM_PROPERTY, "false"); + System.setProperty(DefaultURIResolver.ALLOW_NETWORK_PROPERTY, "false"); + try { + DefaultURIResolver resolver = new DefaultURIResolver(); + for (String location : new String[] {"http://example.com/x.xsd", + "https://example.com/x.xsd", + "file:///legit/local.xsd", + "jar:file:///a.jar!/x.xsd", + "sub/x.xsd"}) { + for (String base : new String[] {null, localBase()}) { + try { + resolver.resolveEntity("urn:x", location, base); + fail("both switches are off, so \"" + location + "\" must be refused."); + } catch (XmlSchemaException expected) { + assertTrue(expected.getMessage(), + expected.getMessage().contains("turned off")); + } + } + } + } finally { + System.clearProperty(DefaultURIResolver.ALLOW_FILE_SYSTEM_PROPERTY); + System.clearProperty(DefaultURIResolver.ALLOW_NETWORK_PROPERTY); + } + } + + @Test + public void testFileSystemResolutionIsAllowedByDefault() { + assertNull(System.getProperty(DefaultURIResolver.ALLOW_FILE_SYSTEM_PROPERTY)); + DefaultURIResolver resolver = new DefaultURIResolver(); + + assertEquals("file:///legit/local.xsd", + resolver.resolveEntity("urn:x", "file:///legit/local.xsd", null).getSystemId()); + assertEquals("sub/x.xsd", + resolver.resolveEntity("urn:x", "sub/x.xsd", null).getSystemId()); + } + + @Test + public void testUnparseableAllowFileSystemValueLeavesResolutionOn() { + System.setProperty(DefaultURIResolver.ALLOW_FILE_SYSTEM_PROPERTY, "off"); + try { + assertEquals("file:///legit/local.xsd", + new DefaultURIResolver() + .resolveEntity("urn:x", "file:///legit/local.xsd", null) + .getSystemId()); + } finally { + System.clearProperty(DefaultURIResolver.ALLOW_FILE_SYSTEM_PROPERTY); + } + } + + private static void assertRefusedWith(DefaultURIResolver resolver, String schemaLocation, + String baseUri, String expectedMessageFragment) { + try { + resolver.resolveEntity("urn:x", schemaLocation, baseUri); + fail("The location \"" + schemaLocation + "\" must be refused."); + } catch (XmlSchemaException expected) { + assertTrue(expected.getMessage(), + expected.getMessage().contains(expectedMessageFragment)); + } + } }
