This is an automated email from the ASF dual-hosted git repository.
coheigea pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ws-xmlschema.git
The following commit(s) were added to refs/heads/master by this push:
new 34ec6d91 Add a new property to disable reading local files (#159)
34ec6d91 is described below
commit 34ec6d91154814eae2a7dbce10863a75b0e2b17b
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Thu Sep 17 13:26:59 2026 +0100
Add a new property to disable reading local files (#159)
---
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));
+ }
+ }
}