From 75ed067f831effa97391027c27d7956628834340 Mon Sep 17 00:00:00 2001 From: Dave Marion Date: Thu, 3 Sep 2026 20:33:00 +0000 Subject: [PATCH 1/6] Added property for allowed URLs for URLContextClassLoaderFactory --- .../core/classloader/ClassLoaderUtil.java | 1 + .../URLContextClassLoaderFactory.java | 34 +++++++++++++++++-- .../ContextClassLoaderFactoryTest.java | 24 ++++++++++++- .../MiniAccumuloClusterClasspathTest.java | 2 ++ 4 files changed, 58 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/org/apache/accumulo/core/classloader/ClassLoaderUtil.java b/core/src/main/java/org/apache/accumulo/core/classloader/ClassLoaderUtil.java index e04b772c37c..d911237fe82 100644 --- a/core/src/main/java/org/apache/accumulo/core/classloader/ClassLoaderUtil.java +++ b/core/src/main/java/org/apache/accumulo/core/classloader/ClassLoaderUtil.java @@ -47,6 +47,7 @@ public static synchronized void initContextFactory(AccumuloConfiguration conf) { LOG.info("Using default {}, which is subject to change in a future release", ContextClassLoaderFactory.class.getName()); FACTORY = new URLContextClassLoaderFactory(); + FACTORY.init(() -> new ConfigurationImpl(conf)); } else { // load user's selected implementation and provide it with the service environment try { diff --git a/core/src/main/java/org/apache/accumulo/core/classloader/URLContextClassLoaderFactory.java b/core/src/main/java/org/apache/accumulo/core/classloader/URLContextClassLoaderFactory.java index 32681e6e5d0..b651a9b945e 100644 --- a/core/src/main/java/org/apache/accumulo/core/classloader/URLContextClassLoaderFactory.java +++ b/core/src/main/java/org/apache/accumulo/core/classloader/URLContextClassLoaderFactory.java @@ -18,14 +18,20 @@ */ package org.apache.accumulo.core.classloader; +import static com.google.common.base.Preconditions.checkArgument; + import java.io.UncheckedIOException; import java.net.MalformedURLException; import java.net.URI; import java.net.URL; import java.net.URLClassLoader; import java.util.Arrays; +import java.util.regex.Pattern; +import org.apache.accumulo.core.conf.Property; +import org.apache.accumulo.core.spi.common.ContextClassLoaderEnvironment; import org.apache.accumulo.core.spi.common.ContextClassLoaderFactory; +import org.apache.accumulo.core.spi.common.ContextClassLoaderFactory.ContextClassLoaderException; import org.apache.accumulo.core.util.cache.Caches; import org.apache.accumulo.core.util.cache.Caches.CacheName; import org.slf4j.Logger; @@ -42,6 +48,9 @@ public class URLContextClassLoaderFactory implements ContextClassLoaderFactory { private static final Logger LOG = LoggerFactory.getLogger(URLContextClassLoaderFactory.class); + public static final String URL_PATTERN_PROPERTY = + Property.GENERAL_ARBITRARY_PROP_PREFIX + "factory.class.loader.url.allowed.patterns"; + // Cache the class loaders for re-use // WeakReferences are used so that the class loaders can be cleaned up when no longer needed // Classes that are loaded contain a reference to the class loader used to load them @@ -49,8 +58,25 @@ public class URLContextClassLoaderFactory implements ContextClassLoaderFactory { private final Cache classloaders = Caches.getInstance().createNewBuilder(CacheName.CLASSLOADERS, true).weakValues().build(); + private volatile Pattern urlPattern = null; + @Override - public ClassLoader getClassLoader(String context) { + public void init(ContextClassLoaderEnvironment env) { + String urlPatternProperty = env.getConfiguration().get(URL_PATTERN_PROPERTY); + if (urlPatternProperty == null) { + LOG.warn("Property " + URL_PATTERN_PROPERTY + " not set, no contexts are allowed"); + } else { + urlPattern = Pattern.compile(urlPatternProperty); + } + } + + @Override + public ClassLoader getClassLoader(String context) throws ContextClassLoaderException { + if (urlPattern == null) { + throw new ContextClassLoaderException( + "Property " + URL_PATTERN_PROPERTY + " not set, no contexts are allowed"); + } + if (context == null) { throw new IllegalArgumentException("Unknown context"); } @@ -59,7 +85,11 @@ public ClassLoader getClassLoader(String context) { LOG.debug("Creating URLClassLoader for context, uris: {}", context); return new URLClassLoader(Arrays.stream(context.split(",")).map(p -> { try { - return URI.create(p).toURL(); + URL url = URI.create(p).toURL(); + checkArgument(urlPattern.matcher(url.toExternalForm()).matches(), + "Context %s URL (%s) not allowed by pattern (%s)", context, url.toExternalForm(), + urlPattern.pattern()); + return url; } catch (MalformedURLException e) { throw new UncheckedIOException(e); } diff --git a/core/src/test/java/org/apache/accumulo/core/classloader/ContextClassLoaderFactoryTest.java b/core/src/test/java/org/apache/accumulo/core/classloader/ContextClassLoaderFactoryTest.java index 440d6ca72b4..ca8360056eb 100644 --- a/core/src/test/java/org/apache/accumulo/core/classloader/ContextClassLoaderFactoryTest.java +++ b/core/src/test/java/org/apache/accumulo/core/classloader/ContextClassLoaderFactoryTest.java @@ -19,6 +19,7 @@ package org.apache.accumulo.core.classloader; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import java.net.URL; import java.net.URLClassLoader; @@ -37,7 +38,8 @@ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -@SuppressFBWarnings(value = "PATH_TRAVERSAL_IN", justification = "paths not set by user input") +@SuppressFBWarnings(value = {"PATH_TRAVERSAL_IN", "NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE"}, + justification = "paths not set by user input") public class ContextClassLoaderFactoryTest extends WithTestNames { @TempDir @@ -45,6 +47,7 @@ public class ContextClassLoaderFactoryTest extends WithTestNames { private URL uri1; private URL uri2; + private String tempFolderPattern; @BeforeEach public void setup() throws Exception { @@ -70,6 +73,24 @@ public void setup() throws Exception { propsFile2.toFile()); uri2 = propsFile2.toUri().toURL(); + tempFolderPattern = ".*/" + tempFolder.getFileName().toString() + "/.*"; + } + + @Test + public void urlContextPatternNotSet() { + ConfigurationCopy cc = new ConfigurationCopy(); + cc.set(Property.GENERAL_CONTEXT_CLASSLOADER_FACTORY.getKey(), + URLContextClassLoaderFactory.class.getName()); + ClassLoaderUtil.resetContextFactoryForTests(); + ClassLoaderUtil.initContextFactory(cc); + ContextClassLoaderException ex = assertThrows(ContextClassLoaderException.class, () -> { + @SuppressWarnings("unused") + URLClassLoader classloader = + (URLClassLoader) ClassLoaderUtil.getContextFactory().getClassLoader(uri1.toString()); + }); + assertEquals( + "Error getting classloader for context: Property general.custom.factory.class.loader.url.allowed.patterns not set, no contexts are allowed", + ex.getMessage()); } @Test @@ -78,6 +99,7 @@ public void differentContexts() throws ContextClassLoaderException { ConfigurationCopy cc = new ConfigurationCopy(); cc.set(Property.GENERAL_CONTEXT_CLASSLOADER_FACTORY.getKey(), URLContextClassLoaderFactory.class.getName()); + cc.set(URLContextClassLoaderFactory.URL_PATTERN_PROPERTY, tempFolderPattern); ClassLoaderUtil.resetContextFactoryForTests(); ClassLoaderUtil.initContextFactory(cc); diff --git a/minicluster/src/test/java/org/apache/accumulo/minicluster/MiniAccumuloClusterClasspathTest.java b/minicluster/src/test/java/org/apache/accumulo/minicluster/MiniAccumuloClusterClasspathTest.java index 88881dee9a0..04d5bc60001 100644 --- a/minicluster/src/test/java/org/apache/accumulo/minicluster/MiniAccumuloClusterClasspathTest.java +++ b/minicluster/src/test/java/org/apache/accumulo/minicluster/MiniAccumuloClusterClasspathTest.java @@ -29,6 +29,7 @@ import java.util.Map; import java.util.Map.Entry; +import org.apache.accumulo.core.classloader.URLContextClassLoaderFactory; import org.apache.accumulo.core.client.Accumulo; import org.apache.accumulo.core.client.AccumuloClient; import org.apache.accumulo.core.client.BatchWriter; @@ -76,6 +77,7 @@ public static void setupMiniCluster() throws Exception { config.setZooKeeperPort(0); HashMap site = new HashMap<>(); site.put(Property.TSERV_WAL_MAX_SIZE.getKey(), "1G"); + site.put(URLContextClassLoaderFactory.URL_PATTERN_PROPERTY, jarFile.toURI().toString()); config.setSiteConfig(site); accumulo = new MiniAccumuloCluster(config); accumulo.start(); From ecd4104a9f2a83c99c2ea7db5b102d99f857afac Mon Sep 17 00:00:00 2001 From: Dave Marion Date: Tue, 8 Sep 2026 12:25:50 +0000 Subject: [PATCH 2/6] Added test for context value not matching pattern --- .../URLContextClassLoaderFactory.java | 1 - .../ContextClassLoaderFactoryTest.java | 17 +++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/org/apache/accumulo/core/classloader/URLContextClassLoaderFactory.java b/core/src/main/java/org/apache/accumulo/core/classloader/URLContextClassLoaderFactory.java index b651a9b945e..088d0e4ca47 100644 --- a/core/src/main/java/org/apache/accumulo/core/classloader/URLContextClassLoaderFactory.java +++ b/core/src/main/java/org/apache/accumulo/core/classloader/URLContextClassLoaderFactory.java @@ -31,7 +31,6 @@ import org.apache.accumulo.core.conf.Property; import org.apache.accumulo.core.spi.common.ContextClassLoaderEnvironment; import org.apache.accumulo.core.spi.common.ContextClassLoaderFactory; -import org.apache.accumulo.core.spi.common.ContextClassLoaderFactory.ContextClassLoaderException; import org.apache.accumulo.core.util.cache.Caches; import org.apache.accumulo.core.util.cache.Caches.CacheName; import org.slf4j.Logger; diff --git a/core/src/test/java/org/apache/accumulo/core/classloader/ContextClassLoaderFactoryTest.java b/core/src/test/java/org/apache/accumulo/core/classloader/ContextClassLoaderFactoryTest.java index ca8360056eb..521d1ea7bc1 100644 --- a/core/src/test/java/org/apache/accumulo/core/classloader/ContextClassLoaderFactoryTest.java +++ b/core/src/test/java/org/apache/accumulo/core/classloader/ContextClassLoaderFactoryTest.java @@ -20,6 +20,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.net.URL; import java.net.URLClassLoader; @@ -93,6 +94,22 @@ public void urlContextPatternNotSet() { ex.getMessage()); } + @Test + public void urlContextPatternDoesNotMath() { + ConfigurationCopy cc = new ConfigurationCopy(); + cc.set(Property.GENERAL_CONTEXT_CLASSLOADER_FACTORY.getKey(), + URLContextClassLoaderFactory.class.getName()); + cc.set(URLContextClassLoaderFactory.URL_PATTERN_PROPERTY, "file://path/to/unknown/folder/.*"); + ClassLoaderUtil.resetContextFactoryForTests(); + ClassLoaderUtil.initContextFactory(cc); + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> { + @SuppressWarnings("unused") + URLClassLoader classloader = + (URLClassLoader) ClassLoaderUtil.getContextFactory().getClassLoader(uri1.toString()); + }); + assertTrue(ex.getMessage().contains(" not allowed by pattern ")); + } + @Test public void differentContexts() throws ContextClassLoaderException { From 7311ddbfde05cf9b494537af4bae7c0cfaadd526 Mon Sep 17 00:00:00 2001 From: Dave Marion Date: Wed, 9 Sep 2026 16:47:53 +0000 Subject: [PATCH 3/6] Updated some failing ITs --- .../URLContextClassLoaderFactory.java | 3 +- .../ContextClassLoaderFactoryTest.java | 8 +++-- .../MiniAccumuloClusterClasspathTest.java | 7 +++-- .../ClassLoaderContextCompactionIT.java | 11 ++++++- .../test/functional/ScannerContextIT.java | 25 ++++++++++++--- .../accumulo/test/shell/ShellServerIT.java | 31 +++++++++++++------ 6 files changed, 62 insertions(+), 23 deletions(-) diff --git a/core/src/main/java/org/apache/accumulo/core/classloader/URLContextClassLoaderFactory.java b/core/src/main/java/org/apache/accumulo/core/classloader/URLContextClassLoaderFactory.java index 088d0e4ca47..ffafd75ef60 100644 --- a/core/src/main/java/org/apache/accumulo/core/classloader/URLContextClassLoaderFactory.java +++ b/core/src/main/java/org/apache/accumulo/core/classloader/URLContextClassLoaderFactory.java @@ -22,7 +22,6 @@ import java.io.UncheckedIOException; import java.net.MalformedURLException; -import java.net.URI; import java.net.URL; import java.net.URLClassLoader; import java.util.Arrays; @@ -84,7 +83,7 @@ public ClassLoader getClassLoader(String context) throws ContextClassLoaderExcep LOG.debug("Creating URLClassLoader for context, uris: {}", context); return new URLClassLoader(Arrays.stream(context.split(",")).map(p -> { try { - URL url = URI.create(p).toURL(); + URL url = new URL(p); checkArgument(urlPattern.matcher(url.toExternalForm()).matches(), "Context %s URL (%s) not allowed by pattern (%s)", context, url.toExternalForm(), urlPattern.pattern()); diff --git a/core/src/test/java/org/apache/accumulo/core/classloader/ContextClassLoaderFactoryTest.java b/core/src/test/java/org/apache/accumulo/core/classloader/ContextClassLoaderFactoryTest.java index 521d1ea7bc1..11c264eda4b 100644 --- a/core/src/test/java/org/apache/accumulo/core/classloader/ContextClassLoaderFactoryTest.java +++ b/core/src/test/java/org/apache/accumulo/core/classloader/ContextClassLoaderFactoryTest.java @@ -22,6 +22,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; import java.nio.file.Files; @@ -74,7 +75,7 @@ public void setup() throws Exception { propsFile2.toFile()); uri2 = propsFile2.toUri().toURL(); - tempFolderPattern = ".*/" + tempFolder.getFileName().toString() + "/.*"; + tempFolderPattern = tempFolder.toUri().toURL().toExternalForm() + ".*"; } @Test @@ -95,11 +96,12 @@ public void urlContextPatternNotSet() { } @Test - public void urlContextPatternDoesNotMath() { + public void urlContextPatternDoesNotMath() throws MalformedURLException { ConfigurationCopy cc = new ConfigurationCopy(); cc.set(Property.GENERAL_CONTEXT_CLASSLOADER_FACTORY.getKey(), URLContextClassLoaderFactory.class.getName()); - cc.set(URLContextClassLoaderFactory.URL_PATTERN_PROPERTY, "file://path/to/unknown/folder/.*"); + cc.set(URLContextClassLoaderFactory.URL_PATTERN_PROPERTY, + new URL("file:///path/to/unknown/folder/.*").toExternalForm()); ClassLoaderUtil.resetContextFactoryForTests(); ClassLoaderUtil.initContextFactory(cc); IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> { diff --git a/minicluster/src/test/java/org/apache/accumulo/minicluster/MiniAccumuloClusterClasspathTest.java b/minicluster/src/test/java/org/apache/accumulo/minicluster/MiniAccumuloClusterClasspathTest.java index 04d5bc60001..d10faf953b9 100644 --- a/minicluster/src/test/java/org/apache/accumulo/minicluster/MiniAccumuloClusterClasspathTest.java +++ b/minicluster/src/test/java/org/apache/accumulo/minicluster/MiniAccumuloClusterClasspathTest.java @@ -77,7 +77,8 @@ public static void setupMiniCluster() throws Exception { config.setZooKeeperPort(0); HashMap site = new HashMap<>(); site.put(Property.TSERV_WAL_MAX_SIZE.getKey(), "1G"); - site.put(URLContextClassLoaderFactory.URL_PATTERN_PROPERTY, jarFile.toURI().toString()); + site.put(URLContextClassLoaderFactory.URL_PATTERN_PROPERTY, + jarFile.toURI().toURL().toExternalForm()); config.setSiteConfig(site); accumulo = new MiniAccumuloCluster(config); accumulo.start(); @@ -97,8 +98,8 @@ public void testPerTableClasspath() throws Exception { final String tableName = testName(); var ntc = new NewTableConfiguration(); - ntc.setProperties( - Map.of(Property.TABLE_CLASSLOADER_CONTEXT.getKey(), jarFile.toURI().toString())); + ntc.setProperties(Map.of(Property.TABLE_CLASSLOADER_CONTEXT.getKey(), + jarFile.toURI().toURL().toExternalForm())); ntc.attachIterator( new IteratorSetting(100, "foocensor", "org.apache.accumulo.test.FooFilter")); diff --git a/test/src/main/java/org/apache/accumulo/test/compaction/ClassLoaderContextCompactionIT.java b/test/src/main/java/org/apache/accumulo/test/compaction/ClassLoaderContextCompactionIT.java index 2162b4eb7af..761b51e5ac6 100644 --- a/test/src/main/java/org/apache/accumulo/test/compaction/ClassLoaderContextCompactionIT.java +++ b/test/src/main/java/org/apache/accumulo/test/compaction/ClassLoaderContextCompactionIT.java @@ -31,6 +31,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.net.MalformedURLException; import java.net.URL; import java.util.EnumSet; import java.util.List; @@ -39,6 +40,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; +import org.apache.accumulo.core.classloader.URLContextClassLoaderFactory; import org.apache.accumulo.core.client.Accumulo; import org.apache.accumulo.core.client.AccumuloClient; import org.apache.accumulo.core.client.IteratorSetting; @@ -95,6 +97,13 @@ public static void after() throws Exception { @Override public void configureMiniCluster(MiniAccumuloConfigImpl cfg, Configuration coreSite) { ExternalCompactionTestUtils.configureMiniCluster(cfg, coreSite); + try { + cfg.setProperty(URLContextClassLoaderFactory.URL_PATTERN_PROPERTY, + new URL("file:" + cfg.getDir().toString() + "/accumulo/classpath/.*").toExternalForm()); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + // After 1 failure start backing off by 5s. // After 3 failures, terminate the Compactor cfg.setProperty(Property.COMPACTOR_FAILURE_BACKOFF_THRESHOLD, "1"); @@ -220,7 +229,7 @@ public void testClassLoaderContextErrorKillsCompactor() throws Exception { // Set the context on the table client.tableOperations().setProperty(table1, Property.TABLE_CLASSLOADER_CONTEXT.getKey(), - dst.toUri().toString()); + dst.toUri().toURL().toExternalForm()); final IteratorSetting cfg = new IteratorSetting(101, "FooFilter", "org.apache.accumulo.test.FooFilter"); diff --git a/test/src/main/java/org/apache/accumulo/test/functional/ScannerContextIT.java b/test/src/main/java/org/apache/accumulo/test/functional/ScannerContextIT.java index 7c7ea5d8490..2d9cb0aab20 100644 --- a/test/src/main/java/org/apache/accumulo/test/functional/ScannerContextIT.java +++ b/test/src/main/java/org/apache/accumulo/test/functional/ScannerContextIT.java @@ -25,11 +25,14 @@ import static org.junit.jupiter.api.Assumptions.assumeTrue; import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; import java.time.Duration; import java.util.Collections; import java.util.Iterator; import java.util.Map.Entry; +import org.apache.accumulo.core.classloader.URLContextClassLoaderFactory; import org.apache.accumulo.core.client.Accumulo; import org.apache.accumulo.core.client.AccumuloClient; import org.apache.accumulo.core.client.BatchScanner; @@ -44,7 +47,9 @@ import org.apache.accumulo.core.data.Value; import org.apache.accumulo.core.security.Authorizations; import org.apache.accumulo.miniclusterImpl.MiniAccumuloClusterImpl; +import org.apache.accumulo.miniclusterImpl.MiniAccumuloConfigImpl; import org.apache.accumulo.test.harness.AccumuloClusterHarness; +import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; import org.junit.jupiter.api.BeforeEach; @@ -64,6 +69,16 @@ protected Duration defaultTimeout() { return Duration.ofMinutes(2); } + @Override + public void configureMiniCluster(MiniAccumuloConfigImpl cfg, Configuration hadoopCoreSite) { + try { + cfg.setProperty(URLContextClassLoaderFactory.URL_PATTERN_PROPERTY, + new URL(CONTEXT_DIR + "/.*").toExternalForm()); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + } + @BeforeEach public void checkCluster() throws Exception { assumeTrue(getClusterType() == ClusterType.MINI); @@ -131,7 +146,7 @@ public void testScanContextOverridesTableContext() throws Exception { // that contains nothing. The ScanContextIT context will point to the test iterators jar String tableContextProperty = Property.TABLE_CLASSLOADER_CONTEXT.getKey(); String tableContextDir = "file://" + System.getProperty("user.dir") + "/target"; - String tableContextClasspath = tableContextDir + "/TestFoo.jar"; + String tableContextClasspath = new URL(tableContextDir + "/TestFoo.jar").toExternalForm(); // Set the ScanContextIT context on the namespace c.namespaceOperations().setProperty(Namespace.DEFAULT.name(), tableContextProperty, CONTEXT); @@ -194,7 +209,7 @@ public void testOneScannerDoesntInterfereWithAnother() throws Exception { IteratorSetting cfg = new IteratorSetting(21, "reverse", "org.apache.accumulo.test.functional.ValueReversingIterator"); one.addScanIterator(cfg); - one.setClassLoaderContext(CONTEXT); + one.setClassLoaderContext(new URL(CONTEXT).toExternalForm()); Iterator> iterator = one.iterator(); for (int i = 0; i < ITERATIONS; i++) { @@ -236,7 +251,7 @@ public void testClearContext() throws Exception { IteratorSetting cfg = new IteratorSetting(21, "reverse", "org.apache.accumulo.test.functional.ValueReversingIterator"); one.addScanIterator(cfg); - one.setClassLoaderContext(CONTEXT); + one.setClassLoaderContext(new URL(CONTEXT).toExternalForm()); Iterator> iterator = one.iterator(); for (int i = 0; i < ITERATIONS; i++) { @@ -264,7 +279,7 @@ private void scanCheck(AccumuloClient c, String tableName, IteratorSetting cfg, String expected) throws Exception { try (Scanner bs = c.createScanner(tableName, Authorizations.EMPTY)) { if (context != null) { - bs.setClassLoaderContext(context); + bs.setClassLoaderContext(new URL(context).toExternalForm()); } if (cfg != null) { bs.addScanIterator(cfg); @@ -284,7 +299,7 @@ private void batchCheck(AccumuloClient c, String tableName, IteratorSetting cfg, try (BatchScanner bs = c.createBatchScanner(tableName)) { bs.setRanges(Collections.singleton(new Range())); if (context != null) { - bs.setClassLoaderContext(context); + bs.setClassLoaderContext(new URL(context).toExternalForm()); } if (cfg != null) { bs.addScanIterator(cfg); diff --git a/test/src/main/java/org/apache/accumulo/test/shell/ShellServerIT.java b/test/src/main/java/org/apache/accumulo/test/shell/ShellServerIT.java index 4283c8d837d..dc88a1130a0 100644 --- a/test/src/main/java/org/apache/accumulo/test/shell/ShellServerIT.java +++ b/test/src/main/java/org/apache/accumulo/test/shell/ShellServerIT.java @@ -35,6 +35,8 @@ import java.io.File; import java.io.IOException; import java.io.PrintWriter; +import java.net.MalformedURLException; +import java.net.URL; import java.nio.file.Files; import java.time.Duration; import java.util.ArrayList; @@ -51,6 +53,7 @@ import java.util.regex.Pattern; import org.apache.accumulo.core.Constants; +import org.apache.accumulo.core.classloader.URLContextClassLoaderFactory; import org.apache.accumulo.core.client.Accumulo; import org.apache.accumulo.core.client.AccumuloClient; import org.apache.accumulo.core.client.AccumuloException; @@ -129,6 +132,13 @@ public class ShellServerIT extends SharedMiniClusterBase { private static class ShellServerITConfigCallback implements MiniClusterConfigurationCallback { @Override public void configureMiniCluster(MiniAccumuloConfigImpl cfg, Configuration coreSite) { + try { + cfg.setProperty(URLContextClassLoaderFactory.URL_PATTERN_PROPERTY, + new URL("file://" + System.getProperty("user.dir") + "/target/*").toExternalForm()); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + // Only one tserver to avoid race conditions on ZK propagation (auths and configuration) cfg.getClusterServerConfiguration().setNumDefaultTabletServers(1); // Set the min span to 0 so we will definitely get all the traces back. See ACCUMULO-4365 @@ -1756,7 +1766,8 @@ public void verifyPerTableClasspath(final String table, final File fooConstraint File fooFilterJar = initJar("/org/apache/accumulo/test/FooFilter.jar", "FooFilter", rootPath); - String context = fooFilterJar.toURI() + "," + fooConstraintJar.toURI(); + String context = fooFilterJar.toURI().toURL().toExternalForm() + "," + + fooConstraintJar.toURI().toURL().toExternalForm(); ts.exec("createtable " + table, true); ts.exec( @@ -1959,10 +1970,11 @@ public void scansWithClassLoaderContext() throws IOException { make10(); setupFakeContextPath(); + String fakeCtx = new URL(FAKE_CONTEXT).toExternalForm(); result = ts.exec("config -t " + tableName + " -s " + Property.TABLE_CLASSLOADER_CONTEXT.getKey() - + "=" + FAKE_CONTEXT); + + "=" + fakeCtx); assertEquals("root@miniInstance " + tableName + "> config -t " + tableName + " -s " - + Property.TABLE_CLASSLOADER_CONTEXT.getKey() + "=" + FAKE_CONTEXT + "\n", result); + + Property.TABLE_CLASSLOADER_CONTEXT.getKey() + "=" + fakeCtx + "\n", result); result = ts.exec("setshelliter -pn baz -n reverse -p 21 -class " + VALUE_REVERSING_ITERATOR); assertTrue(result.contains("The iterator class does not implement OptionDescriber")); @@ -1991,25 +2003,26 @@ public void scansWithClassLoaderContext() throws IOException { // Override the table classloader context with the REAL implementation of // ValueReversingIterator, which does reverse the value. - result = ts.exec("scan -pn baz -b row1 -e row1 -cc " + REAL_CONTEXT); + String realCtx = new URL(REAL_CONTEXT).toExternalForm(); + result = ts.exec("scan -pn baz -b row1 -e row1 -cc " + realCtx); assertEquals(2, result.split("\n").length); assertTrue(result.contains("eulav")); assertFalse(result.contains("value")); - result = ts.exec("scan -pn baz -b row3 -e row5 -cc " + REAL_CONTEXT); + result = ts.exec("scan -pn baz -b row3 -e row5 -cc " + realCtx); assertEquals(4, result.split("\n").length); assertTrue(result.contains("eulav")); assertFalse(result.contains("value")); - result = ts.exec("scan -pn baz -r row3 -cc " + REAL_CONTEXT); + result = ts.exec("scan -pn baz -r row3 -cc " + realCtx); assertEquals(2, result.split("\n").length); assertTrue(result.contains("eulav")); assertFalse(result.contains("value")); - result = ts.exec("scan -pn baz -b row: -cc " + REAL_CONTEXT); + result = ts.exec("scan -pn baz -b row: -cc " + realCtx); assertEquals(1, result.split("\n").length); - result = ts.exec("scan -pn baz -b row -cc " + REAL_CONTEXT); + result = ts.exec("scan -pn baz -b row -cc " + realCtx); assertEquals(11, result.split("\n").length); assertTrue(result.contains("eulav")); assertFalse(result.contains("value")); - result = ts.exec("scan -pn baz -e row: -cc " + REAL_CONTEXT); + result = ts.exec("scan -pn baz -e row: -cc " + realCtx); assertEquals(11, result.split("\n").length); assertTrue(result.contains("eulav")); assertFalse(result.contains("value")); From 29e5802a6031e40ef4112b416864f10f7768c62a Mon Sep 17 00:00:00 2001 From: Dave Marion Date: Fri, 11 Sep 2026 15:42:49 +0000 Subject: [PATCH 4/6] Fix regex in ShellServerIT --- .../main/java/org/apache/accumulo/test/shell/ShellServerIT.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/src/main/java/org/apache/accumulo/test/shell/ShellServerIT.java b/test/src/main/java/org/apache/accumulo/test/shell/ShellServerIT.java index dc88a1130a0..ea0fec9a9c4 100644 --- a/test/src/main/java/org/apache/accumulo/test/shell/ShellServerIT.java +++ b/test/src/main/java/org/apache/accumulo/test/shell/ShellServerIT.java @@ -134,7 +134,7 @@ private static class ShellServerITConfigCallback implements MiniClusterConfigura public void configureMiniCluster(MiniAccumuloConfigImpl cfg, Configuration coreSite) { try { cfg.setProperty(URLContextClassLoaderFactory.URL_PATTERN_PROPERTY, - new URL("file://" + System.getProperty("user.dir") + "/target/*").toExternalForm()); + new URL("file://" + System.getProperty("user.dir") + "/target/.*").toExternalForm()); } catch (MalformedURLException e) { throw new RuntimeException(e); } From 2e8e0b32cb806a0bd45f073fae8acf54b6e9d007 Mon Sep 17 00:00:00 2001 From: Dave Marion Date: Thu, 1 Oct 2026 13:07:26 +0000 Subject: [PATCH 5/6] Normalize property and paths, add test for input variances --- .../URLContextClassLoaderFactory.java | 23 +++-- .../URLContextClassLoaderFactoryTest.java | 83 +++++++++++++++++++ 2 files changed, 100 insertions(+), 6 deletions(-) create mode 100644 core/src/test/java/org/apache/accumulo/core/classloader/URLContextClassLoaderFactoryTest.java diff --git a/core/src/main/java/org/apache/accumulo/core/classloader/URLContextClassLoaderFactory.java b/core/src/main/java/org/apache/accumulo/core/classloader/URLContextClassLoaderFactory.java index ffafd75ef60..03cfcdd5758 100644 --- a/core/src/main/java/org/apache/accumulo/core/classloader/URLContextClassLoaderFactory.java +++ b/core/src/main/java/org/apache/accumulo/core/classloader/URLContextClassLoaderFactory.java @@ -22,6 +22,8 @@ import java.io.UncheckedIOException; import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; import java.net.URLClassLoader; import java.util.Arrays; @@ -64,10 +66,21 @@ public void init(ContextClassLoaderEnvironment env) { if (urlPatternProperty == null) { LOG.warn("Property " + URL_PATTERN_PROPERTY + " not set, no contexts are allowed"); } else { - urlPattern = Pattern.compile(urlPatternProperty); + urlPattern = + Pattern.compile(urlPatternProperty.replaceAll(":///", ":/").replaceAll("://", ":/")); } } + // visible for tests + protected URL testContextAgainstPattern(String context) + throws MalformedURLException, URISyntaxException { + URL url = new URI(context).normalize().toURL(); + checkArgument(urlPattern.matcher(url.toExternalForm()).matches(), + "Context %s URL (%s) not allowed by pattern (%s)", context, url.toExternalForm(), + urlPattern.pattern()); + return url; + } + @Override public ClassLoader getClassLoader(String context) throws ContextClassLoaderException { if (urlPattern == null) { @@ -83,13 +96,11 @@ public ClassLoader getClassLoader(String context) throws ContextClassLoaderExcep LOG.debug("Creating URLClassLoader for context, uris: {}", context); return new URLClassLoader(Arrays.stream(context.split(",")).map(p -> { try { - URL url = new URL(p); - checkArgument(urlPattern.matcher(url.toExternalForm()).matches(), - "Context %s URL (%s) not allowed by pattern (%s)", context, url.toExternalForm(), - urlPattern.pattern()); - return url; + return testContextAgainstPattern(p); } catch (MalformedURLException e) { throw new UncheckedIOException(e); + } catch (URISyntaxException e) { + throw new IllegalArgumentException(e); } }).toArray(URL[]::new), ClassLoader.getSystemClassLoader()); }); diff --git a/core/src/test/java/org/apache/accumulo/core/classloader/URLContextClassLoaderFactoryTest.java b/core/src/test/java/org/apache/accumulo/core/classloader/URLContextClassLoaderFactoryTest.java new file mode 100644 index 00000000000..b8bc1fccbea --- /dev/null +++ b/core/src/test/java/org/apache/accumulo/core/classloader/URLContextClassLoaderFactoryTest.java @@ -0,0 +1,83 @@ +/* + * 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 + * + * https://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.accumulo.core.classloader; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.util.Map; + +import org.apache.accumulo.core.spi.common.ContextClassLoaderEnvironment; +import org.apache.accumulo.core.spi.common.ServiceEnvironment.Configuration; +import org.junit.jupiter.api.Test; + +public class URLContextClassLoaderFactoryTest { + + @Test + public void test() throws MalformedURLException, URISyntaxException { + URLContextClassLoaderFactory factory = new URLContextClassLoaderFactory(); + factory.init(new ContextClassLoaderEnvironment() { + + @Override + public Configuration getConfiguration() { + return Configuration.from(Map.of(URLContextClassLoaderFactory.URL_PATTERN_PROPERTY, + "file:/path/a/.*|file:///path/a/.*|http://path/b/.*"), false); + } + + }); + + assertNotNull(factory.testContextAgainstPattern("file:///path/a/application_jars/.*")); + assertNotNull(factory.testContextAgainstPattern("file:/path/a/application_jars/.*")); + assertNotNull(factory.testContextAgainstPattern("file:///path/b/../a/application_jars/.*")); + assertNotNull(factory.testContextAgainstPattern("file:/path/b/../a/application_jars/.*")); + IllegalArgumentException iae = assertThrows(IllegalArgumentException.class, + () -> factory.testContextAgainstPattern("file:///path/b/application_jars/.*")); + assertTrue(iae.getMessage().contains("not allowed by pattern")); + iae = assertThrows(IllegalArgumentException.class, + () -> factory.testContextAgainstPattern("file:/path/b/application_jars/.*")); + assertTrue(iae.getMessage().contains("not allowed by pattern")); + iae = assertThrows(IllegalArgumentException.class, + () -> factory.testContextAgainstPattern("file:///path/a/../b/application_jars/.*")); + assertTrue(iae.getMessage().contains("not allowed by pattern")); + iae = assertThrows(IllegalArgumentException.class, + () -> factory.testContextAgainstPattern("file:///path/a/../c/application_jars/.*")); + assertTrue(iae.getMessage().contains("not allowed by pattern")); + + assertNotNull(factory.testContextAgainstPattern("http:///path/b/application_jars/.*")); + assertNotNull(factory.testContextAgainstPattern("http:/path/b/application_jars/.*")); + assertNotNull(factory.testContextAgainstPattern("http:///path/c/../b/application_jars/.*")); + assertNotNull(factory.testContextAgainstPattern("http:/path/c/../b/application_jars/.*")); + iae = assertThrows(IllegalArgumentException.class, + () -> factory.testContextAgainstPattern("http:///path/a/application_jars/.*")); + assertTrue(iae.getMessage().contains("not allowed by pattern")); + iae = assertThrows(IllegalArgumentException.class, + () -> factory.testContextAgainstPattern("http:/path/a/application_jars/.*")); + assertTrue(iae.getMessage().contains("not allowed by pattern")); + iae = assertThrows(IllegalArgumentException.class, + () -> factory.testContextAgainstPattern("http:///path/b/../c/application_jars/.*")); + assertTrue(iae.getMessage().contains("not allowed by pattern")); + iae = assertThrows(IllegalArgumentException.class, + () -> factory.testContextAgainstPattern("http:///path/b/../c/application_jars/.*")); + assertTrue(iae.getMessage().contains("not allowed by pattern")); + + } +} From 6a59c84835691a65e24fdbba4755e7c559c23ae5 Mon Sep 17 00:00:00 2001 From: Dave Marion Date: Thu, 1 Oct 2026 19:32:23 +0000 Subject: [PATCH 6/6] Updated with better logic Assisted-by: OpenAI GPT-6 Luna --- .../URLContextClassLoaderFactory.java | 38 ++++++-- .../URLContextClassLoaderFactoryTest.java | 94 +++++++++++-------- 2 files changed, 87 insertions(+), 45 deletions(-) diff --git a/core/src/main/java/org/apache/accumulo/core/classloader/URLContextClassLoaderFactory.java b/core/src/main/java/org/apache/accumulo/core/classloader/URLContextClassLoaderFactory.java index 03cfcdd5758..4da049d237b 100644 --- a/core/src/main/java/org/apache/accumulo/core/classloader/URLContextClassLoaderFactory.java +++ b/core/src/main/java/org/apache/accumulo/core/classloader/URLContextClassLoaderFactory.java @@ -20,12 +20,13 @@ import static com.google.common.base.Preconditions.checkArgument; +import java.io.IOException; import java.io.UncheckedIOException; -import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.net.URLClassLoader; +import java.nio.file.Path; import java.util.Arrays; import java.util.regex.Pattern; @@ -34,6 +35,8 @@ import org.apache.accumulo.core.spi.common.ContextClassLoaderFactory; import org.apache.accumulo.core.util.cache.Caches; import org.apache.accumulo.core.util.cache.Caches.CacheName; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.FileSystem; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -51,6 +54,8 @@ public class URLContextClassLoaderFactory implements ContextClassLoaderFactory { public static final String URL_PATTERN_PROPERTY = Property.GENERAL_ARBITRARY_PROP_PREFIX + "factory.class.loader.url.allowed.patterns"; + private static final Pattern ENCODED_PATH_TRAVERSAL = Pattern.compile("(?i)%(?:25|2e|2f|5c)"); + // Cache the class loaders for re-use // WeakReferences are used so that the class loaders can be cleaned up when no longer needed // Classes that are loaded contain a reference to the class loader used to load them @@ -72,9 +77,28 @@ public void init(ContextClassLoaderEnvironment env) { } // visible for tests - protected URL testContextAgainstPattern(String context) - throws MalformedURLException, URISyntaxException { - URL url = new URI(context).normalize().toURL(); + protected URL testContextAgainstPattern(String context) throws IOException, URISyntaxException { + final URI uri = new URI(context).normalize(); + URL url; + if ("file".equalsIgnoreCase(uri.getScheme())) { + // Match and return the actual filesystem target, not the possibly traversing URL spelling. + // toRealPath also resolves symbolic links, so an allowed path cannot escape via a symlink. + Path realPath = Path.of(uri).toRealPath(); + url = realPath.toUri().toURL(); + } else if ("hdfs".equalsIgnoreCase(uri.getScheme())) { + // Match and return the actual filesystem target, not the possibly traversing URL spelling. + // resolvePath also resolves symbolic links, so an allowed path cannot escape via a symlink. + // This may subsequently fail if the HdfsURLStreamHandlerProvider is not on the classpath + org.apache.hadoop.fs.Path resolved = + FileSystem.get(uri, new Configuration()).resolvePath(new org.apache.hadoop.fs.Path(uri)); + url = resolved.toUri().toURL(); + } else { + String rawPath = uri.getRawPath(); + checkArgument(rawPath == null || !ENCODED_PATH_TRAVERSAL.matcher(rawPath).find(), + "Context %s contains encoded path traversal characters in URL path (%s)", context, + rawPath); + url = uri.toURL(); + } checkArgument(urlPattern.matcher(url.toExternalForm()).matches(), "Context %s URL (%s) not allowed by pattern (%s)", context, url.toExternalForm(), urlPattern.pattern()); @@ -94,10 +118,10 @@ public ClassLoader getClassLoader(String context) throws ContextClassLoaderExcep return classloaders.get(context, k -> { LOG.debug("Creating URLClassLoader for context, uris: {}", context); - return new URLClassLoader(Arrays.stream(context.split(",")).map(p -> { + return new URLClassLoader(Arrays.stream(context.split(",")).map(ctx -> { try { - return testContextAgainstPattern(p); - } catch (MalformedURLException e) { + return testContextAgainstPattern(ctx); + } catch (IOException e) { throw new UncheckedIOException(e); } catch (URISyntaxException e) { throw new IllegalArgumentException(e); diff --git a/core/src/test/java/org/apache/accumulo/core/classloader/URLContextClassLoaderFactoryTest.java b/core/src/test/java/org/apache/accumulo/core/classloader/URLContextClassLoaderFactoryTest.java index b8bc1fccbea..09707e3002c 100644 --- a/core/src/test/java/org/apache/accumulo/core/classloader/URLContextClassLoaderFactoryTest.java +++ b/core/src/test/java/org/apache/accumulo/core/classloader/URLContextClassLoaderFactoryTest.java @@ -18,66 +18,84 @@ */ package org.apache.accumulo.core.classloader; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import java.net.MalformedURLException; -import java.net.URISyntaxException; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.Map; import org.apache.accumulo.core.spi.common.ContextClassLoaderEnvironment; import org.apache.accumulo.core.spi.common.ServiceEnvironment.Configuration; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; public class URLContextClassLoaderFactoryTest { @Test - public void test() throws MalformedURLException, URISyntaxException { - URLContextClassLoaderFactory factory = new URLContextClassLoaderFactory(); - factory.init(new ContextClassLoaderEnvironment() { + public void test(@TempDir Path tempDir) throws Exception { + Path allowedDir = tempDir.resolve("allowed"); + Path allowedJars = allowedDir.resolve("application_jars"); + Files.createDirectories(allowedJars); + Path allowedJar = Files.createFile(allowedJars.resolve("allowed.jar")); + Path outsideJar = Files.createFile(tempDir.resolve("outside.jar")); - @Override - public Configuration getConfiguration() { - return Configuration.from(Map.of(URLContextClassLoaderFactory.URL_PATTERN_PROPERTY, - "file:/path/a/.*|file:///path/a/.*|http://path/b/.*"), false); - } + String allowedFilePattern = allowedDir.toUri().toURL().toExternalForm() + ".*"; + URLContextClassLoaderFactory factory = + factoryForPattern(allowedFilePattern + "|http://path/b/.*"); - }); + // Existing allowed files pass, including paths with literal dot segments that normalize + // to an allowed file. + URL expectedCanonicalUrl = allowedJar.toRealPath().toUri().toURL(); + assertEquals(expectedCanonicalUrl, + factory.testContextAgainstPattern(allowedJar.toUri().toString())); + assertEquals(expectedCanonicalUrl, factory.testContextAgainstPattern( + allowedDir.toUri().toString() + "application_jars/../application_jars/allowed.jar")); + assertThrows(IOException.class, () -> factory + .testContextAgainstPattern(allowedJars.resolve("missing.jar").toUri().toString())); - assertNotNull(factory.testContextAgainstPattern("file:///path/a/application_jars/.*")); - assertNotNull(factory.testContextAgainstPattern("file:/path/a/application_jars/.*")); - assertNotNull(factory.testContextAgainstPattern("file:///path/b/../a/application_jars/.*")); - assertNotNull(factory.testContextAgainstPattern("file:/path/b/../a/application_jars/.*")); - IllegalArgumentException iae = assertThrows(IllegalArgumentException.class, - () -> factory.testContextAgainstPattern("file:///path/b/application_jars/.*")); - assertTrue(iae.getMessage().contains("not allowed by pattern")); - iae = assertThrows(IllegalArgumentException.class, - () -> factory.testContextAgainstPattern("file:/path/b/application_jars/.*")); - assertTrue(iae.getMessage().contains("not allowed by pattern")); - iae = assertThrows(IllegalArgumentException.class, - () -> factory.testContextAgainstPattern("file:///path/a/../b/application_jars/.*")); - assertTrue(iae.getMessage().contains("not allowed by pattern")); - iae = assertThrows(IllegalArgumentException.class, - () -> factory.testContextAgainstPattern("file:///path/a/../c/application_jars/.*")); - assertTrue(iae.getMessage().contains("not allowed by pattern")); + // Traversal that resolves outside the allowed directory is rejected, whether written + // literally, percent-encoded, or reached through a symlink. + assertThrows(IllegalArgumentException.class, + () -> factory.testContextAgainstPattern(allowedDir.toUri() + "../outside.jar")); + assertThrows(IllegalArgumentException.class, + () -> factory.testContextAgainstPattern(allowedDir.toUri() + "%2e%2e/outside.jar")); + Path symlink = Files.createSymbolicLink(allowedDir.resolve("escape.jar"), outsideJar); + assertThrows(IllegalArgumentException.class, + () -> factory.testContextAgainstPattern(symlink.toUri().toString())); + // HTTP URLs retain literal dot-segment normalization, but encoded path delimiters, dots, and + // percent escapes that could be double-decoded are rejected as ambiguous. assertNotNull(factory.testContextAgainstPattern("http:///path/b/application_jars/.*")); - assertNotNull(factory.testContextAgainstPattern("http:/path/b/application_jars/.*")); - assertNotNull(factory.testContextAgainstPattern("http:///path/c/../b/application_jars/.*")); assertNotNull(factory.testContextAgainstPattern("http:/path/c/../b/application_jars/.*")); + IllegalArgumentException iae = assertThrows(IllegalArgumentException.class, + () -> factory.testContextAgainstPattern("http:///path/b/%2e%2e/c/application_jars/.*")); + assertTrue(iae.getMessage().contains("encoded path traversal")); + assertThrows(IllegalArgumentException.class, + () -> factory.testContextAgainstPattern("http:///path/b/%2f../c/application_jars/.*")); + assertThrows(IllegalArgumentException.class, + () -> factory.testContextAgainstPattern("http:///path/b/%252e%252e/c/application_jars/.*")); + iae = assertThrows(IllegalArgumentException.class, () -> factory.testContextAgainstPattern("http:///path/a/application_jars/.*")); assertTrue(iae.getMessage().contains("not allowed by pattern")); - iae = assertThrows(IllegalArgumentException.class, - () -> factory.testContextAgainstPattern("http:/path/a/application_jars/.*")); - assertTrue(iae.getMessage().contains("not allowed by pattern")); - iae = assertThrows(IllegalArgumentException.class, - () -> factory.testContextAgainstPattern("http:///path/b/../c/application_jars/.*")); - assertTrue(iae.getMessage().contains("not allowed by pattern")); - iae = assertThrows(IllegalArgumentException.class, - () -> factory.testContextAgainstPattern("http:///path/b/../c/application_jars/.*")); - assertTrue(iae.getMessage().contains("not allowed by pattern")); + } + private URLContextClassLoaderFactory factoryForPattern(String pattern) { + URLContextClassLoaderFactory factory = new URLContextClassLoaderFactory(); + factory.init(new ContextClassLoaderEnvironment() { + + @Override + public Configuration getConfiguration() { + return Configuration + .from(Map.of(URLContextClassLoaderFactory.URL_PATTERN_PROPERTY, pattern), false); + } + + }); + return factory; } }