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 4da24588 Add a switch to allow network resolution to be turned off 
(#158)
4da24588 is described below

commit 4da245885d6cfd53f950f4af911af40f2ff7862f
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Thu Sep 17 11:25:48 2026 +0100

    Add a switch to allow network resolution to be turned off (#158)
---
 README.txt                                         | 16 ++++++
 THREAT-MODEL.md                                    | 21 +++++++-
 .../schema/resolver/DefaultURIResolver.java        | 53 +++++++++++++++++--
 .../test/java/tests/DefaultURIResolverTest.java    | 59 ++++++++++++++++++++++
 4 files changed, 144 insertions(+), 5 deletions(-)

diff --git a/README.txt b/README.txt
index 0d002be6..28643daa 100644
--- a/README.txt
+++ b/README.txt
@@ -91,6 +91,16 @@ adjust the per-document limits:
       Maximum bytes accepted from one remote schema fetch. The default is
       67108864 (64 MB).
 
+    org.apache.ws.commons.schema.remote.allowNetwork
+      Whether a schema location may be fetched over the network at all. The
+      default is true. Set it to false in a deployment whose schema sets are
+      entirely local: an xs:import naming an http or https location is then
+      refused rather than fetched, and no resolver has to be supplied to get
+      that. Only true and false are recognised, so a typo leaves resolution
+      working rather than quietly turning it off. Local file: and jar: reads
+      are unaffected either way, so this is not on its own a defence against
+      an untrusted schema document - see the Security section below.
+
   file: and jar: locations are read as before, without buffering.
 
   The collections returned by the "read-only" accessors on the schema model
@@ -126,6 +136,12 @@ For example, set a limit with:
   file is fetched on request. It does refuse a file: location that names a
   non-local authority, and a jar: archive fetched over the network.
 
+  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.
+
   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 99f3410e..2cb0be48 100644
--- a/THREAT-MODEL.md
+++ b/THREAT-MODEL.md
@@ -285,6 +285,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.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.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 |
@@ -482,7 +483,11 @@ matching disclaimer.
   is not enforceable at the `resolveEntity` boundary. The caller is
   responsible for installing a restricting `URIResolver` if the input
   schema is attacker-controlled *(documented: `DefaultURIResolver.java`;
-  ratified — §14 Q12)*.
+  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.
 - **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
@@ -559,7 +564,11 @@ The embedding Java application **must**:
    is not a supported production posture for untrusted schema bytes. A
    resolver that returns `null` declines the location (the collection
    falls back to any schema already registered for that namespace); one
-   that throws rejects the read outright.
+   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.
 2. When passing a pre-parsed `Document` / `Element` into
    `XmlSchemaCollection.read(...)`, use a `DocumentBuilderFactory`
    hardened against XXE — specifically with `disallow-doctype-decl=true`
@@ -731,6 +740,14 @@ 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 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
+  existing deployment. It is a revision trigger under the first bullet
+  above and is recorded in §5a, §9 and §10 item 1. It does not disturb
+  the §14 Q12(b) ruling: the shipped default still resolves remote
+  locations, and the opt-out does not restrict local `file:` reads, so a
+  report against the default remains `BY-DESIGN: property-disclaimed`.
 - **2026-09-17** — "Place default limits on read timeouts + size on
   remote schemas" (#152) is a revision trigger under the first bullet
   above: a network location is now fetched through a stream the resolver
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 91558b9a..ff8d6b83 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
@@ -45,7 +45,9 @@ import org.xml.sax.InputSource;
  * schemes it will resolve to <code>http</code>, <code>https</code>, 
<code>file</code> and
  * <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. Within the schemes it does allow it
+ * 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
  * 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
@@ -78,6 +80,18 @@ public class DefaultURIResolver implements 
CollectionURIResolver {
     public static final String MAX_BYTES_PROPERTY =
         "org.apache.ws.commons.schema.remote.maxBytes";
 
+    /**
+     * Whether a schema location may be fetched over the network at all. Set 
it to
+     * <code>false</code> in a deployment whose schema sets are entirely 
local: an
+     * <code>xs:import</code> naming an <code>http</code> or 
<code>https</code> location is then
+     * refused instead of fetched, without the deployment having to supply its 
own
+     * {@link URIResolver}. It defaults to <code>true</code>, which is the 
behaviour this resolver
+     * has always had. Only "true" and "false" are recognised, so a typo 
leaves resolution working
+     * rather than silently turning it off.
+     */
+    public static final String ALLOW_NETWORK_PROPERTY =
+        "org.apache.ws.commons.schema.remote.allowNetwork";
+
     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;
@@ -90,6 +104,7 @@ public class DefaultURIResolver implements 
CollectionURIResolver {
     private final long maxFetchMillis =
         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 String collectionBaseURI;
 
@@ -272,6 +287,33 @@ public class DefaultURIResolver implements 
CollectionURIResolver {
         return millis > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int)millis;
     }
 
+    /**
+     * Reads a boolean system property. Only "true" and "false" count, so an 
unparseable value
+     * leaves the default in place rather than being read as 
<code>false</code> the way
+     * {@link Boolean#parseBoolean} would.
+     */
+    private static boolean getBooleanProperty(final String name, boolean 
defaultValue) {
+        try {
+            String value = AccessController.doPrivileged(new 
PrivilegedAction<String>() {
+                public String run() {
+                    return System.getProperty(name);
+                }
+            });
+            if (value != null) {
+                String trimmed = value.trim();
+                if ("true".equalsIgnoreCase(trimmed)) {
+                    return true;
+                }
+                if ("false".equalsIgnoreCase(trimmed)) {
+                    return false;
+                }
+            }
+        } catch (RuntimeException e) {
+            // fall through to the default
+        }
+        return defaultValue;
+    }
+
     private static long getLongProperty(final String name, long defaultValue) {
         try {
             String value = AccessController.doPrivileged(new 
PrivilegedAction<String>() {
@@ -291,7 +333,7 @@ public class DefaultURIResolver implements 
CollectionURIResolver {
         return defaultValue;
     }
 
-    private static void verifyComposedUrl(boolean remoteBase, String 
originalBaseUri, URL base,
+    private void verifyComposedUrl(boolean remoteBase, String originalBaseUri, 
URL base,
                                           URL composed, String schemaLocation) 
{
         verifyPermittedLocation(composed.toString(), schemaLocation);
         final String composedScheme = 
composed.getProtocol().toLowerCase(Locale.ENGLISH);
@@ -328,7 +370,7 @@ public class DefaultURIResolver implements 
CollectionURIResolver {
      * @param uri the resolved location that would be handed to the parser.
      * @param schemaLocation the original schema location, for the error 
message.
      */
-    private static void verifyPermittedLocation(String uri, String 
schemaLocation) {
+    private void verifyPermittedLocation(String uri, String schemaLocation) {
         final String scheme = effectiveScheme(uri);
         if (scheme == null || !ALLOWED_SCHEMES.contains(scheme)) {
             throw new XmlSchemaException("The schema location \"" + 
schemaLocation
@@ -350,6 +392,11 @@ public class DefaultURIResolver implements 
CollectionURIResolver {
                                          + archive + "\", which is not 
permitted by"
                                          + " DefaultURIResolver.");
         }
+        if (!allowNetwork && isNetworkScheme(scheme)) {
+            throw new XmlSchemaException("The schema location \"" + 
schemaLocation
+                                         + "\" would be fetched over the 
network, which "
+                                         + ALLOW_NETWORK_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 4e394092..a07f991b 100644
--- a/xmlschema-core/src/test/java/tests/DefaultURIResolverTest.java
+++ b/xmlschema-core/src/test/java/tests/DefaultURIResolverTest.java
@@ -294,4 +294,63 @@ public class DefaultURIResolverTest extends Assert {
         assertEquals("dir/a:b.xsd",
                      resolver.resolveEntity("urn:x", "dir/a:b.xsd", 
null).getSystemId());
     }
+
+    /**
+     * A deployment whose schema sets are all local can turn remote fetching 
off without writing
+     * its own resolver. The property is read when the resolver is 
constructed, as the per-fetch
+     * bounds are, so each case builds its resolver after setting it.
+     */
+    @Test
+    public void testNetworkResolutionCanBeTurnedOff() {
+        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"}) 
{
+                for (String base : new String[] {null, localBase()}) {
+                    try {
+                        resolver.resolveEntity("urn:x", location, base);
+                        fail("network resolution is off, so \"" + location
+                             + "\" must be refused.");
+                    } catch (XmlSchemaException expected) {
+                        assertTrue(expected.getMessage(),
+                                   expected.getMessage().contains("turned 
off"));
+                    }
+                }
+            }
+            // Local resolution is unaffected: that is the point of the switch.
+            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());
+            assertTrue(resolver.resolveEntity("urn:x", 
"jar:file:///a.jar!/x.xsd", null)
+                           .getSystemId().startsWith("jar:file:"));
+        } finally {
+            System.clearProperty(DefaultURIResolver.ALLOW_NETWORK_PROPERTY);
+        }
+    }
+
+    @Test
+    public void testNetworkResolutionIsAllowedByDefault() {
+        
assertNull(System.getProperty(DefaultURIResolver.ALLOW_NETWORK_PROPERTY));
+
+        assertEquals("http://example.com/x.xsd";,
+                     new DefaultURIResolver()
+                         .resolveEntity("urn:x", "http://example.com/x.xsd";, 
null).getSystemId());
+    }
+
+    @Test
+    public void testUnparseableAllowNetworkValueLeavesResolutionOn() {
+        // Boolean.parseBoolean would read this as false and quietly break the 
deployment.
+        System.setProperty(DefaultURIResolver.ALLOW_NETWORK_PROPERTY, "no");
+        try {
+            assertEquals("http://example.com/x.xsd";,
+                         new DefaultURIResolver()
+                             .resolveEntity("urn:x", 
"http://example.com/x.xsd";, null)
+                             .getSystemId());
+        } finally {
+            System.clearProperty(DefaultURIResolver.ALLOW_NETWORK_PROPERTY);
+        }
+    }
 }

Reply via email to