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));
+        }
+    }
 }

Reply via email to