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