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-neethi.git
commit 696e2489d2054eea01ba644ce4202121dd4ed6ef Author: Colm O hEigeartaigh <[email protected]> AuthorDate: Tue Apr 21 09:54:32 2026 +0100 Only allow http/https for remote policy referenes --- .../java/org/apache/neethi/PolicyReference.java | 20 ++- .../org/apache/neethi/PolicyReferenceTest.java | 165 +++++++++++++++++++++ .../samples/test-policy-reference.xml | 27 ++++ 3 files changed, 206 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/apache/neethi/PolicyReference.java b/src/main/java/org/apache/neethi/PolicyReference.java index ea62c93..5490208 100644 --- a/src/main/java/org/apache/neethi/PolicyReference.java +++ b/src/main/java/org/apache/neethi/PolicyReference.java @@ -136,9 +136,19 @@ public class PolicyReference implements PolicyComponent { } public Policy getRemoteReferencedPolicy(String u) { + URL url; + try { + url = new URL(u); + } catch (MalformedURLException mue) { + throw new RuntimeException("Malformed uri."); + } + + String scheme = url.getProtocol(); + if (!"http".equalsIgnoreCase(scheme) && !"https".equalsIgnoreCase(scheme)) { + throw new RuntimeException("Unsupported URI scheme: only http and https are permitted."); + } + try { - //create java.net URL pointing to remote resource - URL url = new URL(u); URLConnection connection = url.openConnection(); connection.setDoInput(true); @@ -152,10 +162,8 @@ public class PolicyReference implements PolicyComponent { } finally { in.close(); } - } catch (MalformedURLException mue) { - throw new RuntimeException("Malformed uri: " + u); - } catch (IOException ioe) { - throw new RuntimeException("Cannot reach remote resource: " + u); + } catch (IOException ioe) { + throw new RuntimeException("Cannot reach remote policy reference."); } } } diff --git a/src/test/java/org/apache/neethi/PolicyReferenceTest.java b/src/test/java/org/apache/neethi/PolicyReferenceTest.java new file mode 100644 index 0000000..6633264 --- /dev/null +++ b/src/test/java/org/apache/neethi/PolicyReferenceTest.java @@ -0,0 +1,165 @@ +/** + * 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.neethi; + +import java.io.File; +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.Test; + +/** + * Tests for PolicyReference URI scheme validation (CWE-918 / SSRF hardening). + * + * getRemoteReferencedPolicy() must only allow http and https schemes and must + * reject all others (file://, ftp://, jar://, etc.) before opening any connection. + */ +public class PolicyReferenceTest extends PolicyTestCase { + + /** + * Loads a policy from test-policy-reference.xml and verifies the policy name + * is parsed correctly. Normalization is not called, so no outbound call is made. + */ + @Test + public void testGetPolicyFromPolicyReferenceFile() throws Exception { + Policy policy = getPolicy("samples/test-policy-reference.xml"); + assertNotNull(policy); + assertEquals("OuterPolicy", policy.getName()); + } + + // ----------------------------------------------------------------------- + // Scheme allow-list: blocked schemes + // ----------------------------------------------------------------------- + + @Test(expected = RuntimeException.class) + public void testFileSchemeIsRejected() throws Exception { + File policyFile = new File(baseDir, testResourceDir + File.separator + + "samples" + File.separator + "test-policy-reference.xml"); + PolicyReference ref = new PolicyReference(policyEngine); + ref.getRemoteReferencedPolicy(policyFile.toURI().toString()); // file:///... + } + + @Test(expected = RuntimeException.class) + public void testFtpSchemeIsRejected() throws Exception { + PolicyReference ref = new PolicyReference(policyEngine); + ref.getRemoteReferencedPolicy("ftp://example.com/policy.xml"); + } + + @Test(expected = RuntimeException.class) + public void testJarSchemeIsRejected() throws Exception { + PolicyReference ref = new PolicyReference(policyEngine); + ref.getRemoteReferencedPolicy("jar:file:///some.jar!/policy.xml"); + } + + @Test(expected = RuntimeException.class) + public void testMalformedUriIsRejected() throws Exception { + PolicyReference ref = new PolicyReference(policyEngine); + ref.getRemoteReferencedPolicy("not a url at all"); + } + + // ----------------------------------------------------------------------- + // Scheme allow-list: ftp:// must not trigger an outbound connection + // ----------------------------------------------------------------------- + + /** + * Verifies that a ftp:// URI is rejected BEFORE any TCP connection is made. + * Opens a local ServerSocket and confirms no connection arrives within the + * timeout after the call is rejected. + */ + @Test + public void testFtpSchemeDoesNotTriggerOutboundConnection() throws Exception { + AtomicBoolean connectionReceived = new AtomicBoolean(false); + CountDownLatch latch = new CountDownLatch(1); + + try (ServerSocket server = new ServerSocket(0)) { + server.setSoTimeout(500); + final int port = server.getLocalPort(); + + Thread acceptThread = new Thread(() -> { + try { + Socket accepted = server.accept(); + connectionReceived.set(true); + accepted.close(); + } catch (IOException ignored) { + // timeout — expected: no connection should arrive + } finally { + latch.countDown(); + } + }); + acceptThread.setDaemon(true); + acceptThread.start(); + + PolicyReference ref = new PolicyReference(policyEngine); + try { + ref.getRemoteReferencedPolicy("ftp://127.0.0.1:" + port + "/policy.xml"); + fail("Expected RuntimeException for ftp:// scheme"); + } catch (RuntimeException expected) { + // correct — rejected before connecting + } + + latch.await(1, TimeUnit.SECONDS); + } + + assertFalse( + "ftp:// scheme triggered an outbound TCP connection — scheme allow-list not enforced", + connectionReceived.get()); + } + + // ----------------------------------------------------------------------- + // Scheme allow-list: http and https are permitted + // ----------------------------------------------------------------------- + + /** + * Verifies that http:// URIs are accepted by the scheme check (the connection + * will fail with a RuntimeException due to the server refusing/not existing, + * but the exception must NOT be about an unsupported scheme). + */ + @Test + public void testHttpSchemeIsPermitted() { + PolicyReference ref = new PolicyReference(policyEngine); + try { + // Use a port that will immediately refuse the connection so the test + // does not block, but the scheme validation must pass. + ref.getRemoteReferencedPolicy("http://127.0.0.1:1/policy.xml"); + fail("Expected RuntimeException due to connection failure"); + } catch (RuntimeException e) { + assertFalse( + "http:// was rejected by scheme allow-list — it should be permitted", + e.getMessage().contains("Unsupported URI scheme")); + } + } + + @Test + public void testHttpsSchemeIsPermitted() { + PolicyReference ref = new PolicyReference(policyEngine); + try { + ref.getRemoteReferencedPolicy("https://127.0.0.1:1/policy.xml"); + fail("Expected RuntimeException due to connection failure"); + } catch (RuntimeException e) { + assertFalse( + "https:// was rejected by scheme allow-list — it should be permitted", + e.getMessage().contains("Unsupported URI scheme")); + } + } +} diff --git a/src/test/test-resources/samples/test-policy-reference.xml b/src/test/test-resources/samples/test-policy-reference.xml new file mode 100644 index 0000000..35b0821 --- /dev/null +++ b/src/test/test-resources/samples/test-policy-reference.xml @@ -0,0 +1,27 @@ +<!-- + ~ 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. + --> +<wsp:Policy xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy" + xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" + Name="OuterPolicy"> + <wsp:ExactlyOne> + <wsp:All> + <wsp:PolicyReference URI="ftp://localhost:12344"/> + </wsp:All> + </wsp:ExactlyOne> +</wsp:Policy>
