Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
2c8b71c
IGNITE-28743: Block remote HTTP/HTTPS/FTP URLs in resolveSpringUrl to…
animovscw Jun 9, 2026
6359859
IGNITE-28743 Validate URL scheme in resolveSpringUrl
animovscw Jun 9, 2026
764048d
IGNITE-28743 Add jdbc tests
animovscw Jun 15, 2026
6698540
IGNITE-28743 Combine three tests into one
animovscw Jun 22, 2026
4806277
IGNITE-28743 Add a test that verifies that no security-exception is t…
animovscw Jun 22, 2026
b586301
IGNITE-28743 Add JDBC end-to-end tests for remote cfg:// URL blocking
animovscw Jun 26, 2026
2af3a6e
IGNITE-28743 Fix code style
animovscw Jun 26, 2026
ac366c0
IGNITE-28743 Fix code style
animovscw Jun 26, 2026
a479bf0
Potential fix for pull request finding
animovscw Jun 30, 2026
50e3d5e
Copilot review suggestions
animovscw Jun 30, 2026
3ab4cfe
IGNITE-28743 checkstyle
animovscw Jul 8, 2026
b4e0d72
IGNITE-28743 checkstyle
animovscw Jul 8, 2026
600c6c9
Potential fix for pull request finding
animovscw Jul 14, 2026
b2b3f26
IGNITE-28869 Potential fix for pull request finding
animovscw Jul 14, 2026
8eedf69
IGNITE-28827 Move ftp validation before try
animovscw Jul 16, 2026
e9c4729
Update modules/core/src/main/java/org/apache/ignite/internal/util/Ign…
animovscw Jul 24, 2026
c42bd91
Update modules/core/src/main/java/org/apache/ignite/internal/util/Ign…
animovscw Jul 24, 2026
1aa536c
Update modules/core/src/main/java/org/apache/ignite/internal/util/Ign…
animovscw Jul 24, 2026
cb9eb9a
Update modules/core/src/main/java/org/apache/ignite/internal/util/Ign…
animovscw Jul 24, 2026
d1f781d
Review changes
animovscw Jul 24, 2026
fa23414
Move the raw-string check into the catch block
animovscw Jul 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,15 @@
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.UUID;
import java.util.concurrent.Callable;
import org.apache.ignite.IgniteSystemProperties;
import org.apache.ignite.configuration.CacheConfiguration;
import org.apache.ignite.configuration.IgniteConfiguration;
import org.apache.ignite.internal.IgniteEx;
import org.apache.ignite.testframework.GridTestUtils;
import org.apache.ignite.testframework.junits.WithSystemProperty;
import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
import org.jetbrains.annotations.NotNull;
import org.junit.Test;
Expand Down Expand Up @@ -122,16 +125,16 @@ public void testWrongNodeId() throws Exception {
final String url = CFG_URL_PREFIX + "nodeId=" + wrongId + '@' + configURL();

GridTestUtils.assertThrows(
log,
new Callable<Object>() {
@Override public Object call() throws Exception {
try (Connection conn = DriverManager.getConnection(url)) {
return conn;
}
log,
new Callable<Object>() {
@Override public Object call() throws Exception {
try (Connection conn = DriverManager.getConnection(url)) {
return conn;
}
},
SQLException.class,
"Failed to establish connection with node (is it a server node?): " + wrongId
}
},
SQLException.class,
"Failed to establish connection with node (is it a server node?): " + wrongId
);
}

Expand All @@ -147,16 +150,16 @@ public void testClientNodeId() throws Exception {
final String url = CFG_URL_PREFIX + "nodeId=" + clientId + '@' + configURL();

GridTestUtils.assertThrows(
log,
new Callable<Object>() {
@Override public Object call() throws Exception {
try (Connection conn = DriverManager.getConnection(url)) {
return conn;
}
log,
new Callable<Object>() {
@Override public Object call() throws Exception {
try (Connection conn = DriverManager.getConnection(url)) {
return conn;
}
},
SQLException.class,
"Failed to establish connection with node (is it a server node?): " + clientId
}
},
SQLException.class,
"Failed to establish connection with node (is it a server node?): " + clientId
);
}

Expand Down Expand Up @@ -296,4 +299,57 @@ public void testSqlHints() throws Exception {
assertTrue(((JdbcConnection)conn).skipReducerOnUpdate());
}
}

/**
* Test that JDBC cfg:// URL with remote HTTP, HTTPS, and FTP location is blocked.
*/
@Test
public void testRemoteCfgUrlsAreBlocked() {
for (String scheme : Arrays.asList("http", "https", "ftp", "ftps")) {
final String url = CFG_URL_PREFIX + scheme + "://attacker.example.com/evil.xml";
final String expMsg = scheme.startsWith("ftp") ? "always blocked" : "Remote Spring configuration URLs";

GridTestUtils.assertThrows(
log,
new Callable<Object>() {
@Override public Object call() throws Exception {
try (Connection conn = DriverManager.getConnection(url)) {
return conn;
}
}
},
SQLException.class,
expMsg
);
}
}

/**
* Test that JDBC cfg:// URL with remote HTTP location is allowed when system property is set.
*/
@Test
@WithSystemProperty(key = IgniteSystemProperties.IGNITE_ALLOW_REMOTE_SPRING_CFG_URL, value = "true")
public void testRemoteHttpCfgUrlAllowedWhenFlagSet() {
final String url = CFG_URL_PREFIX + "http://127.0.0.1:1/nonexistent.xml";

Throwable err = GridTestUtils.assertThrows(
log,
new Callable<Object>() {
@Override public Object call() throws Exception {
try (Connection conn = DriverManager.getConnection(url)) {
return conn;
}
}
},
SQLException.class,
null
);

String msg = err.getMessage();

assertFalse(
"Security exception should not be thrown when flag is enabled",
msg != null && msg.contains("Remote Spring configuration URLs")
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1908,6 +1908,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|FTPS are always blocked regardless of this property due to security 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.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,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<String> 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<String> 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());

Expand Down Expand Up @@ -2558,12 +2566,72 @@ public static boolean mkdirs(File dir) {
public static URL resolveSpringUrl(String springCfgPath) throws IgniteCheckedException {
A.notNull(springCfgPath, "springCfgPath");

String prop = IgniteSystemProperties.IGNITE_ALLOW_REMOTE_SPRING_CFG_URL;

URL url;

try {
url = new URL(springCfgPath);

URL cfgUrl = url;

String scheme = cfgUrl.getProtocol().toLowerCase(Locale.ROOT);

// Unwrap jar:<nested_url>!/path (potentially nested) to avoid bypassing remote-scheme checks.
while ("jar".equals(scheme)) {
String file = cfgUrl.getFile();

int sep = file.indexOf("!/");

if (sep <= 0)
break;

try {
cfgUrl = new URL(file.substring(0, sep));
scheme = cfgUrl.getProtocol().toLowerCase(Locale.ROOT);
}
catch (MalformedURLException ignored) {
break;
}
}

if (REMOTE_CFG_SCHEMES.contains(scheme)) {
if (ALWAYS_BLOCKED_CFG_SCHEMES.contains(scheme))
throw new IgniteCheckedException(
"Spring configuration URL`s with scheme '" + scheme + "' are always blocked " +
"due to security risk. Use a local file or classpath reference instead. " +
"For remote HTTP|HTTPS set system property: -D" +
prop + "=true. " +
"Provided host: " + cfgUrl.getHost()
);

boolean allowRemote = IgniteSystemProperties.getBoolean(prop);

if (!allowRemote)
throw new IgniteCheckedException(
"Remote Spring configuration URL`s (http|https) are not allowed by default " +
"to prevent remote code execution via attacker-controlled Spring XML. " +
"Provided host: " + cfgUrl.getHost() + ". " +
"To allow remote URL`s set system property: -D" +
prop + "=true"
);
}
}
catch (MalformedURLException e) {
// "ftps" is not a recognized scheme for java.net.URL, so it lands here rather
// than being caught by the scheme check above. Block it explicitly with the
// same security message for a consistent user-facing error.
String lowerPath = springCfgPath.toLowerCase(Locale.ROOT);

for (String blockedScheme : ALWAYS_BLOCKED_CFG_SCHEMES) {
if (lowerPath.startsWith(blockedScheme + "://"))
throw new IgniteCheckedException(
"Spring configuration URL`s with scheme '" + blockedScheme + "' are always blocked " +
"due to security risk. Use a local file or classpath reference instead. " +
"For remote HTTP|HTTPS set system property: -D" +
prop + "=true.", e
);
}
url = resolveIgniteUrl(springCfgPath);

if (url == null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import java.math.BigInteger;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
Expand Down Expand Up @@ -1615,6 +1616,78 @@ 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 = IgniteSystemProperties.IGNITE_ALLOW_REMOTE_SPRING_CFG_URL, value = "true")
public void testResolveSpringUrlAllowsHttpWhenPropertySet() throws IgniteCheckedException {
URL url = IgniteUtils.resolveSpringUrl("http://127.0.0.1:1/nonexistent.xml");

assertNotNull(url);
assertEquals("http", url.getProtocol());
}

/**
* Test that FTP is always blocked even when remote URL property is set.
*/
@Test
@WithSystemProperty(key = IgniteSystemProperties.IGNITE_ALLOW_REMOTE_SPRING_CFG_URL, 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("-");
Expand Down
Loading