This is an automated email from the ASF dual-hosted git repository.

coheigea pushed a commit to branch 4.1.x-fixes
in repository https://gitbox.apache.org/repos/asf/cxf.git


The following commit(s) were added to refs/heads/4.1.x-fixes by this push:
     new a4014ad5d09 Restrict valid URL protocols in TLSParameterJaxBUtils and 
URIResolver (#3091)
a4014ad5d09 is described below

commit a4014ad5d097de9e99976213a503bc43df6ae3ab
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Fri May 8 15:58:13 2026 +0100

    Restrict valid URL protocols in TLSParameterJaxBUtils and URIResolver 
(#3091)
    
    (cherry picked from commit 062ebd17a745b2c2028aeae47c0fc92ad0c66a1f)
---
 .../configuration/jsse/TLSParameterJaxBUtils.java  | 23 +++++-
 .../java/org/apache/cxf/resource/URIResolver.java  | 40 ++++++++++
 .../jsse/TLSParameterJaxBUtilsTest.java            | 93 ++++++++++++++++++++++
 .../org/apache/cxf/resource/URIResolverTest.java   | 45 +++++++++++
 .../cxf/wsdl11/AbstractWrapperWSDLLocator.java     | 13 +--
 5 files changed, 202 insertions(+), 12 deletions(-)

diff --git 
a/core/src/main/java/org/apache/cxf/configuration/jsse/TLSParameterJaxBUtils.java
 
b/core/src/main/java/org/apache/cxf/configuration/jsse/TLSParameterJaxBUtils.java
index 9dc81b1236c..45e734e2e06 100644
--- 
a/core/src/main/java/org/apache/cxf/configuration/jsse/TLSParameterJaxBUtils.java
+++ 
b/core/src/main/java/org/apache/cxf/configuration/jsse/TLSParameterJaxBUtils.java
@@ -69,6 +69,9 @@ public final class TLSParameterJaxBUtils {
     private static final Logger LOG =
         LogUtils.getL7dLogger(TLSParameterJaxBUtils.class);
 
+    private static final java.util.Set<String> ALLOWED_KEYSTORE_SCHEMES =
+        java.util.Collections.unmodifiableSet(new 
java.util.HashSet<>(java.util.Arrays.asList("file", "https")));
+
     private TLSParameterJaxBUtils() {
         // empty
     }
@@ -163,7 +166,7 @@ public final class TLSParameterJaxBUtils {
                 keyStore.load(is, password);
             }
         } else if (kst.isSetUrl()) {
-            keyStore.load(new URL(kst.getUrl()).openStream(), password);
+            keyStore.load(openKeystoreUrl(kst.getUrl()), password);
         } else {
             final String loc;
             if (trustStore) {
@@ -215,11 +218,27 @@ public final class TLSParameterJaxBUtils {
             return createTrustStore(is, type);
         }
         if (pst.isSetUrl()) {
-            return createTrustStore(new URL(pst.getUrl()).openStream(), type);
+            return createTrustStore(openKeystoreUrl(pst.getUrl()), type);
         }
         throw new IllegalArgumentException("Could not create KeyStore based on 
information in CertStoreType");
     }
 
+    /**
+     * Opens a URL for keystore loading after validating that its scheme is in 
the
+     * allowlist ({@code file}, {@code https}).  Schemes such as {@code ftp},
+     * {@code http}, {@code jar}, or {@code jndi} are rejected to prevent SSRF.
+     */
+    private static InputStream openKeystoreUrl(String urlString) throws 
IOException {
+        URL url = new URL(urlString);
+        String scheme = url.getProtocol();
+        if (!ALLOWED_KEYSTORE_SCHEMES.contains(scheme)) {
+            throw new IllegalArgumentException(
+                "Keystore URL scheme '" + scheme + "' is not permitted. "
+                + "Allowed schemes: " + ALLOWED_KEYSTORE_SCHEMES);
+        }
+        return url.openStream();
+    }
+
     private static InputStream getResourceAsStream(String resource) {
         InputStream is = ClassLoaderUtils.getResourceAsStream(resource, 
TLSParameterJaxBUtils.class);
         if (is == null) {
diff --git a/core/src/main/java/org/apache/cxf/resource/URIResolver.java 
b/core/src/main/java/org/apache/cxf/resource/URIResolver.java
index 30bc6a682fe..f2c5e7a180c 100644
--- a/core/src/main/java/org/apache/cxf/resource/URIResolver.java
+++ b/core/src/main/java/org/apache/cxf/resource/URIResolver.java
@@ -32,8 +32,13 @@ import java.net.URLDecoder;
 import java.nio.file.Files;
 import java.security.AccessController;
 import java.security.PrivilegedAction;
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Locale;
 import java.util.Map;
+import java.util.Set;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
@@ -56,6 +61,11 @@ import org.apache.cxf.helpers.LoadingByteArrayOutputStream;
  */
 public class URIResolver implements AutoCloseable {
     private static final Logger LOG = LogUtils.getLogger(URIResolver.class);
+    private static final String ALLOWED_URL_SCHEMES_PROPERTY =
+        "org.apache.cxf.resource.uriresolver.allowedSchemes";
+    private static final Set<String> DEFAULT_ALLOWED_URL_SCHEMES =
+        Collections.unmodifiableSet(
+            new HashSet<>(Arrays.asList("file", "http", "https", "jar", "zip", 
"wsjar", "local", "classpath")));
 
     private Map<String, LoadingByteArrayOutputStream> cache = new HashMap<>();
     private File file;
@@ -168,6 +178,7 @@ public class URIResolver implements AutoCloseable {
             if (relative.isAbsolute()) {
                 uri = relative;
                 url = relative.toURL();
+                checkAllowedScheme(url);
 
                 try {
                     HttpURLConnection huc = createInputStream();
@@ -258,6 +269,7 @@ public class URIResolver implements AutoCloseable {
     }
 
     private HttpURLConnection createInputStream() throws IOException {
+        checkAllowedScheme(url);
         HttpURLConnection huc = (HttpURLConnection)url.openConnection();
 
         String host = SystemPropertyAction.getPropertyOrNull("http.proxyHost");
@@ -344,6 +356,7 @@ public class URIResolver implements AutoCloseable {
         }
 
         url = new URL(uriStr);
+        checkAllowedScheme(url);
         try {
             is = url.openStream();
             try {
@@ -409,6 +422,7 @@ public class URIResolver implements AutoCloseable {
         try {
             LoadingByteArrayOutputStream bout = cache.get(uriStr);
             url = new URL(uriStr);
+            checkAllowedScheme(url);
             uri = new URI(url.toString());
             if (bout == null) {
                 URLConnection connection = url.openConnection();
@@ -424,6 +438,32 @@ public class URIResolver implements AutoCloseable {
         }
     }
 
+    private static void checkAllowedScheme(URL targetUrl) throws IOException {
+        String scheme = targetUrl.getProtocol();
+        if (scheme == null) {
+            return;
+        }
+        scheme = scheme.toLowerCase(Locale.ROOT);
+        if (!getAllowedSchemes().contains(scheme)) {
+            throw new IOException("URL scheme '" + scheme + "' is not 
permitted for URIResolver. "
+                                  + "Allowed schemes: " + getAllowedSchemes());
+        }
+    }
+
+    public static Set<String> getAllowedSchemes() {
+        Set<String> allowed = new HashSet<>(DEFAULT_ALLOWED_URL_SCHEMES);
+        String configured = 
SystemPropertyAction.getPropertyOrNull(ALLOWED_URL_SCHEMES_PROPERTY);
+        if (configured != null) {
+            for (String entry : configured.split(",")) {
+                String scheme = entry.trim().toLowerCase(Locale.ROOT);
+                if (!scheme.isEmpty()) {
+                    allowed.add(scheme);
+                }
+            }
+        }
+        return allowed;
+    }
+
     public URI getURI() {
         return uri;
     }
diff --git 
a/core/src/test/java/org/apache/cxf/configuration/jsse/TLSParameterJaxBUtilsTest.java
 
b/core/src/test/java/org/apache/cxf/configuration/jsse/TLSParameterJaxBUtilsTest.java
new file mode 100644
index 00000000000..49706cdc58e
--- /dev/null
+++ 
b/core/src/test/java/org/apache/cxf/configuration/jsse/TLSParameterJaxBUtilsTest.java
@@ -0,0 +1,93 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.cxf.configuration.jsse;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.KeyStore;
+
+import org.apache.cxf.configuration.security.KeyStoreType;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+
+/**
+ * Unit tests for TLSParameterJaxBUtils keystore loading.
+ *
+ * <p>CXF-SSRF-001 — {@link TLSParameterJaxBUtils#getKeyStore(KeyStoreType, 
boolean)} passes the
+ * configured {@code url} attribute directly to {@code new 
URL(kst.getUrl()).openStream()} without
+ * validating the scheme.  This allows any protocol understood by the JDK URL 
handler (including
+ * {@code ftp:}, {@code jar:}, {@code jndi:}, …) to be used as the keystore 
source, creating a
+ * Server-Side Request Forgery (SSRF) surface.</p>
+ */
+public class TLSParameterJaxBUtilsTest {
+
+    @Test
+    public void testFileBasedJksKeystoreIsLoaded() throws Exception {
+        final String password = "changeit";
+        final Path tempJks = Files.createTempFile("cxf-filestore-", ".jks");
+
+        try {
+            KeyStore source = KeyStore.getInstance("JKS");
+            source.load(null, password.toCharArray());
+            try (OutputStream os = Files.newOutputStream(tempJks)) {
+                source.store(os, password.toCharArray());
+            }
+
+            KeyStoreType kst = new KeyStoreType();
+            kst.setType("JKS");
+            kst.setPassword(password);
+            kst.setFile(tempJks.toString());
+
+            KeyStore loaded = TLSParameterJaxBUtils.getKeyStore(kst, false);
+            assertNotNull(loaded);
+            assertEquals(0, loaded.size());
+        } finally {
+            Files.deleteIfExists(tempJks);
+        }
+    }
+
+    /**
+     * Verifies that {@code getKeyStore()} rejects an {@code ftp://} URL with 
an
+     * {@link IllegalArgumentException}.  Prior to the fix, the URL was passed 
directly
+     * to {@code new URL(…).openStream()} without scheme validation, allowing 
any JDK-supported
+     * protocol (ftp, jar, jndi, …) to be used as a keystore source (SSRF).
+     */
+    @Test
+    public void testFtpUrlAllowedErroneouslyInKeystoreUrl() throws Exception {
+        KeyStoreType kst = new KeyStoreType();
+        kst.setUrl("ftp://127.0.0.1:12345/keystore.jks";);
+
+        try {
+            TLSParameterJaxBUtils.getKeyStore(kst, false);
+            fail("Expected IllegalArgumentException for ftp:// keystore URL — 
scheme not in allowlist");
+        } catch (IllegalArgumentException e) {
+            // expected — ftp:// is correctly rejected
+        } catch (IOException e) {
+            fail("Expected IllegalArgumentException but got IOException, "
+                    + "meaning ftp:// was accepted and an outbound connection 
was attempted: " + e);
+        }
+    }
+
+}
diff --git a/core/src/test/java/org/apache/cxf/resource/URIResolverTest.java 
b/core/src/test/java/org/apache/cxf/resource/URIResolverTest.java
index b6bb0669371..adc21742a45 100644
--- a/core/src/test/java/org/apache/cxf/resource/URIResolverTest.java
+++ b/core/src/test/java/org/apache/cxf/resource/URIResolverTest.java
@@ -20,7 +20,11 @@
 package org.apache.cxf.resource;
 
 import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.ServerSocket;
+import java.net.Socket;
 import java.net.URL;
+import java.nio.charset.StandardCharsets;
 
 import org.apache.cxf.helpers.IOUtils;
 
@@ -31,6 +35,7 @@ import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 public class URIResolverTest {
 
@@ -186,6 +191,46 @@ public class URIResolverTest {
         resolveWithCheckingClassloaderInConstructor(null, 
"wsdl/folder%20with%20spaces/foo.wsdl", this.getClass());
     }
 
+    @Test
+    public void testFtpProtocolRejectedByDefault() throws Exception {
+        try {
+            new URIResolver("ftp://127.0.0.1:12345/example.wsdl";);
+            fail("Expected IOException for disallowed ftp:// scheme");
+        } catch (java.io.IOException ex) {
+            assertTrue(ex.getMessage().contains("ftp"));
+            assertTrue(ex.getMessage().contains("not permitted"));
+        }
+    }
+
+    @Test
+    public void testHttpProtocolStillAllowed() throws Exception {
+        checkingThreadThrowable = null;
+        try (ServerSocket serverSocket = new ServerSocket(0)) {
+            Thread t = new Thread(() -> {
+                try (Socket socket = serverSocket.accept();
+                     OutputStream os = socket.getOutputStream()) {
+                    os.write(("HTTP/1.1 200 OK\r\n"
+                              + "Content-Length: 4\r\n"
+                              + "Connection: close\r\n\r\n"
+                              + "test").getBytes(StandardCharsets.US_ASCII));
+                    os.flush();
+                } catch (Throwable th) {
+                    checkingThreadThrowable = th;
+                }
+            });
+            t.start();
+
+            URIResolver resolver = new URIResolver("http://127.0.0.1:"; + 
serverSocket.getLocalPort() + "/schema.xsd");
+            assertTrue(resolver.isResolved());
+            assertNotNull(resolver.getInputStream());
+            resolver.close();
+
+            t.join(5000);
+            assertFalse("HTTP server thread did not finish in time", 
t.isAlive());
+            assertNull(checkingThreadThrowable);
+        }
+    }
+
     private void resolveWithCheckingClassloader(URIResolver resolver, String 
baseUriStr, String uriStr,
             Class<?> callingCls) throws InterruptedException {
         checkingThreadThrowable = null;
diff --git 
a/rt/wsdl/src/main/java/org/apache/cxf/wsdl11/AbstractWrapperWSDLLocator.java 
b/rt/wsdl/src/main/java/org/apache/cxf/wsdl11/AbstractWrapperWSDLLocator.java
index 81a53fd76d5..0b16b42f435 100644
--- 
a/rt/wsdl/src/main/java/org/apache/cxf/wsdl11/AbstractWrapperWSDLLocator.java
+++ 
b/rt/wsdl/src/main/java/org/apache/cxf/wsdl11/AbstractWrapperWSDLLocator.java
@@ -21,21 +21,14 @@ package org.apache.cxf.wsdl11;
 import java.io.IOException;
 import java.net.URI;
 import java.net.URISyntaxException;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
 
 import javax.wsdl.xml.WSDLLocator;
 
 import org.xml.sax.InputSource;
 
-public abstract class AbstractWrapperWSDLLocator implements WSDLLocator {
-    private static final List<String> ALLOWED_SCHEMES;
+import org.apache.cxf.resource.URIResolver;
 
-    static {
-        List<String> schemes = Arrays.asList("file", "http", "https", 
"classpath", "jar");
-        ALLOWED_SCHEMES = Collections.unmodifiableList(schemes);
-    }
+public abstract class AbstractWrapperWSDLLocator implements WSDLLocator {
 
     protected WSDLLocator parent;
     String wsdlUrl;
@@ -98,7 +91,7 @@ public abstract class AbstractWrapperWSDLLocator implements 
WSDLLocator {
         // Do a check on the scheme to see if it's anything that could be a 
security risk
         try {
             URI url = new URI(importLocation);
-            if (!(url.getScheme() == null || 
ALLOWED_SCHEMES.contains(url.getScheme()))) {
+            if (!(url.getScheme() == null || 
URIResolver.getAllowedSchemes().contains(url.getScheme()))) {
                 throw new IllegalArgumentException("The " + url.getScheme() + 
" URI scheme is not allowed");
             }
         } catch (URISyntaxException e) {

Reply via email to