Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -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.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<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 @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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("-");
Expand Down