diff --git a/modules/clients/src/test/java/org/apache/ignite/internal/jdbc2/JdbcConnectionSelfTest.java b/modules/clients/src/test/java/org/apache/ignite/internal/jdbc2/JdbcConnectionSelfTest.java index b8056bfaca7a8..96f1bdc727402 100644 --- a/modules/clients/src/test/java/org/apache/ignite/internal/jdbc2/JdbcConnectionSelfTest.java +++ b/modules/clients/src/test/java/org/apache/ignite/internal/jdbc2/JdbcConnectionSelfTest.java @@ -296,4 +296,67 @@ public void testSqlHints() throws Exception { assertTrue(((JdbcConnection)conn).skipReducerOnUpdate()); } } + + /** + * Test that JDBC cfg:// URL with remote HTTP location is blocked by default to prevent RCE. + */ + @Test + public void testRemoteHttpCfgUrlIsBlocked() { + final String url = CFG_URL_PREFIX + "http://attacker.example.com/evil.xml"; + + GridTestUtils.assertThrows( + log, + new Callable() { + @Override public Object call() throws Exception { + try (Connection conn = DriverManager.getConnection(url)) { + return conn; + } + } + }, + SQLException.class, + "Remote Spring configuration URLs" + ); + } + + /** + * Test that JDBC cfg:// URL with remote HTTPS location is blocked by default to prevent RCE. + */ + @Test + public void testRemoteHttpsCfgUrlIsBlocked() { + final String url = CFG_URL_PREFIX + "https://attacker.example.com/evil.xml"; + + GridTestUtils.assertThrows( + log, + new Callable() { + @Override public Object call() throws Exception { + try (Connection conn = DriverManager.getConnection(url)) { + return conn; + } + } + }, + SQLException.class, + "Remote Spring configuration URLs" + ); + } + + /** + * Test that JDBC cfg:// URL with FTP location is always blocked. + */ + @Test + public void testFtpCfgUrlIsAlwaysBlocked() { + final String url = CFG_URL_PREFIX + "ftp://attacker.example.com/evil.xml"; + + GridTestUtils.assertThrows( + log, + new Callable() { + @Override public Object call() throws Exception { + try (Connection conn = DriverManager.getConnection(url)) { + return conn; + } + } + }, + SQLException.class, + "always blocked" + ); + } } diff --git a/modules/core/src/main/java/org/apache/ignite/IgniteSystemProperties.java b/modules/core/src/main/java/org/apache/ignite/IgniteSystemProperties.java index b1c36a9015827..8c52a124ede20 100644 --- a/modules/core/src/main/java/org/apache/ignite/IgniteSystemProperties.java +++ b/modules/core/src/main/java/org/apache/ignite/IgniteSystemProperties.java @@ -1967,6 +1967,15 @@ public final class IgniteSystemProperties extends IgniteCommonsSystemProperties @SystemProperty(value = "Packages list to expose in configuration view") public static final String IGNITE_CONFIGURATION_VIEW_PACKAGES = "IGNITE_CONFIGURATION_VIEW_PACKAGES"; + + /** + * System property to allow remote HTTP/HTTPS URLs when loading Spring XML configuration. + * Remote URLs are blocked by default to prevent RCE via attacker-controlled Spring XML. + * FTP is always blocked regardless of this property due to MITM risk. + */ + @SystemProperty(value = "Allow remote HTTP/HTTPS URLs when loading Spring XML configuration") + public static final String IGNITE_ALLOW_REMOTE_SPRING_CFG_URL = "ignite.spring.cfg.allowRemoteUrl"; + /** * Enforces singleton. */ diff --git a/modules/core/src/main/java/org/apache/ignite/internal/util/IgniteUtils.java b/modules/core/src/main/java/org/apache/ignite/internal/util/IgniteUtils.java index 1513346138589..fd18f13a161e0 100755 --- a/modules/core/src/main/java/org/apache/ignite/internal/util/IgniteUtils.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/util/IgniteUtils.java @@ -387,6 +387,14 @@ public abstract class IgniteUtils extends CommonUtils { /** Ignite Work Directory. */ public static final String IGNITE_WORK_DIR = System.getenv(IgniteSystemProperties.IGNITE_WORK_DIR); + /** URL schemes that load remote content and are blocked by default in Spring configuration. */ + private static final Set REMOTE_CFG_SCHEMES = Collections.unmodifiableSet( + new HashSet<>(Arrays.asList("http", "https", "ftp", "ftps"))); + + /** URL schemes that are always blocked regardless of system property due to security risk. */ + private static final Set ALWAYS_BLOCKED_CFG_SCHEMES = Collections.unmodifiableSet( + new HashSet<>(Arrays.asList("ftp", "ftps"))); + /** Random is used to get random server node to authentication from client node. */ private static final Random RND = new Random(System.currentTimeMillis()); @@ -2600,6 +2608,31 @@ public static URL resolveSpringUrl(String springCfgPath) throws IgniteCheckedExc try { url = new URL(springCfgPath); + + String scheme = url.getProtocol().toLowerCase(); + + if (REMOTE_CFG_SCHEMES.contains(scheme)) { + // FTP is always blocked — unencrypted, susceptible to MITM + if (ALWAYS_BLOCKED_CFG_SCHEMES.contains(scheme)) + throw new IgniteCheckedException( + "Spring configuration URLs with scheme '" + scheme + "' are always blocked " + + "due to security risk (unencrypted transfer, MITM vulnerability). " + + "Use HTTPS or a local file/classpath reference instead. " + + "Provided host: " + url.getHost() + ); + + // HTTP/HTTPS blocked by default, allowed via system property + boolean allowRemote = Boolean.getBoolean(IgniteSystemProperties.IGNITE_ALLOW_REMOTE_SPRING_CFG_URL); + + if (!allowRemote) + throw new IgniteCheckedException( + "Remote Spring configuration URLs (http/https) are not allowed by default " + + "to prevent remote code execution via attacker-controlled Spring XML. " + + "Provided host: " + url.getHost() + ". " + + "To allow remote URLs set system property: -D" + + IgniteSystemProperties.IGNITE_ALLOW_REMOTE_SPRING_CFG_URL + "=true" + ); + } } catch (MalformedURLException e) { url = resolveIgniteUrl(springCfgPath); diff --git a/modules/core/src/test/java/org/apache/ignite/internal/util/IgniteUtilsSelfTest.java b/modules/core/src/test/java/org/apache/ignite/internal/util/IgniteUtilsSelfTest.java index 19f6ad17c38d8..e0149ef69bd41 100644 --- a/modules/core/src/test/java/org/apache/ignite/internal/util/IgniteUtilsSelfTest.java +++ b/modules/core/src/test/java/org/apache/ignite/internal/util/IgniteUtilsSelfTest.java @@ -1615,6 +1615,85 @@ public void testLongToBytes() { } } + /** + * Test that remote HTTPS URL in Spring cfg is blocked by default. + */ + @Test + public void testResolveSpringUrlBlocksHttpsByDefault() { + assertThrows(log, () -> { + IgniteUtils.resolveSpringUrl("https://attacker.example.com/evil.xml"); + return null; + }, IgniteCheckedException.class, "Remote Spring configuration URLs"); + } + + /** + * Test that remote FTP URL in Spring cfg is blocked by default. + */ + @Test + public void testResolveSpringUrlBlocksFtpByDefault() { + assertThrows(log, () -> { + IgniteUtils.resolveSpringUrl("ftp://attacker.example.com/evil.xml"); + return null; + }, IgniteCheckedException.class, "always blocked"); + } + + /** + * Test that remote HTTP URL in Spring cfg is blocked by default + * and error message contains guidance on how to enable remote URLs. + */ + @Test + public void testResolveSpringUrlBlocksHttpByDefault() { + try { + IgniteUtils.resolveSpringUrl("http://attacker.example.com/evil.xml"); + fail("Expected IgniteCheckedException"); + } + catch (IgniteCheckedException e) { + assertTrue( + "Error message should contain system property name", + e.getMessage().contains(IgniteSystemProperties.IGNITE_ALLOW_REMOTE_SPRING_CFG_URL) + ); + assertFalse( + "Error message should not contain full URL to avoid credential leak", + e.getMessage().contains("http://attacker.example.com/evil.xml") + ); + assertTrue( + "Error message should contain host", + e.getMessage().contains("attacker.example.com") + ); + } + } + + /** + * Test that remote HTTP URL is allowed when system property is set. + */ + @Test + @WithSystemProperty(key = "ignite.spring.cfg.allowRemoteUrl", value = "true") + public void testResolveSpringUrlAllowsHttpWhenPropertySet() { + // Should not throw — validation passes when flag is true. + // Will throw MalformedURLException or connection error, not our security check. + try { + IgniteUtils.resolveSpringUrl("http://127.0.0.1:1/nonexistent.xml"); + } + catch (IgniteCheckedException e) { + assertFalse( + "Should not throw security exception when flag is enabled", + e.getMessage().contains("Remote Spring configuration URLs") + ); + } + } + + /** + * Test that FTP is always blocked even when remote URL property is set. + */ + @Test + @WithSystemProperty(key = "ignite.spring.cfg.allowRemoteUrl", value = "true") + public void testResolveSpringUrlFtpAlwaysBlocked() { + assertThrows(log, () -> { + IgniteUtils.resolveSpringUrl("ftp://attacker.example.com/evil.xml"); + return null; + }, IgniteCheckedException.class, "always blocked"); + } + /** */ private byte[] asByteArray(String text) { String[] split = text.split("-");