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 @@ -20,12 +20,14 @@
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.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 All @@ -36,10 +38,14 @@
* Connection test.
*/
public class JdbcConnectionSelfTest extends GridCommonAbstractTest {
/** Custom cache name. */
/**
* Custom cache name.
*/
private static final String CUSTOM_CACHE_NAME = "custom-cache";

/** Grid count. */
/**
* Grid count.
*/
private static final int GRID_CNT = 2;

/**
Expand All @@ -49,8 +55,11 @@ protected String configURL() {
return "modules/clients/src/test/config/jdbc-config.xml";
}

/** {@inheritDoc} */
@Override protected IgniteConfiguration getConfiguration(String igniteInstanceName) throws Exception {
/**
* {@inheritDoc}
*/
@Override
protected IgniteConfiguration getConfiguration(String igniteInstanceName) throws Exception {
IgniteConfiguration cfg = super.getConfiguration(igniteInstanceName);

cfg.setCacheConfiguration(cacheConfiguration(DEFAULT_CACHE_NAME), cacheConfiguration(CUSTOM_CACHE_NAME));
Expand All @@ -71,8 +80,11 @@ private CacheConfiguration cacheConfiguration(@NotNull String name) throws Excep
return cfg;
}

/** {@inheritDoc} */
@Override protected void beforeTestsStarted() throws Exception {
/**
* {@inheritDoc}
*/
@Override
protected void beforeTestsStarted() throws Exception {
startGridsMultiThreaded(GRID_CNT);
}

Expand All @@ -85,12 +97,12 @@ public void testDefaults() throws Exception {

try (Connection conn = DriverManager.getConnection(url)) {
assertNotNull(conn);
assertTrue(((JdbcConnection)conn).ignite().configuration().isClientMode());
assertTrue(((JdbcConnection) conn).ignite().configuration().isClientMode());
}

try (Connection conn = DriverManager.getConnection(url + '/')) {
assertNotNull(conn);
assertTrue(((JdbcConnection)conn).ignite().configuration().isClientMode());
assertTrue(((JdbcConnection) conn).ignite().configuration().isClientMode());
}
}

Expand Down Expand Up @@ -124,7 +136,8 @@ public void testWrongNodeId() throws Exception {
GridTestUtils.assertThrows(
log,
new Callable<Object>() {
@Override public Object call() throws Exception {
@Override
public Object call() throws Exception {
try (Connection conn = DriverManager.getConnection(url)) {
return conn;
}
Expand All @@ -149,7 +162,8 @@ public void testClientNodeId() throws Exception {
GridTestUtils.assertThrows(
log,
new Callable<Object>() {
@Override public Object call() throws Exception {
@Override
public Object call() throws Exception {
try (Connection conn = DriverManager.getConnection(url)) {
return conn;
}
Expand Down Expand Up @@ -180,16 +194,17 @@ public void testWrongCache() throws Exception {
final String url = CFG_URL_PREFIX + "cache=wrongCacheName@" + 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,
"Client is invalid. Probably cache name is wrong."
},
SQLException.class,
"Client is invalid. Probably cache name is wrong."
);
}

Expand All @@ -209,16 +224,17 @@ public void testClose() throws Exception {
assertTrue(conn.isClosed());

GridTestUtils.assertThrows(
log,
new Callable<Object>() {
@Override public Object call() throws Exception {
conn.isValid(2);
log,
new Callable<Object>() {
@Override
public Object call() throws Exception {
conn.isValid(2);

return null;
}
},
SQLException.class,
"Connection is closed."
return null;
}
},
SQLException.class,
"Connection is closed."
);
}
}
Expand Down Expand Up @@ -265,35 +281,82 @@ public void testTxAllowedRollback() throws Exception {
@Test
public void testSqlHints() throws Exception {
try (final Connection conn = DriverManager.getConnection(CFG_URL_PREFIX + "enforceJoinOrder=true@"
+ configURL())) {
assertTrue(((JdbcConnection)conn).isEnforceJoinOrder());
assertFalse(((JdbcConnection)conn).isDistributedJoins());
assertFalse(((JdbcConnection)conn).isCollocatedQuery());
assertFalse(((JdbcConnection)conn).skipReducerOnUpdate());
+ configURL())) {
assertTrue(((JdbcConnection) conn).isEnforceJoinOrder());
assertFalse(((JdbcConnection) conn).isDistributedJoins());
assertFalse(((JdbcConnection) conn).isCollocatedQuery());
assertFalse(((JdbcConnection) conn).skipReducerOnUpdate());
}

try (final Connection conn = DriverManager.getConnection(CFG_URL_PREFIX + "distributedJoins=true@"
+ configURL())) {
assertFalse(((JdbcConnection)conn).isEnforceJoinOrder());
assertTrue(((JdbcConnection)conn).isDistributedJoins());
assertFalse(((JdbcConnection)conn).isCollocatedQuery());
assertFalse(((JdbcConnection)conn).skipReducerOnUpdate());
+ configURL())) {
assertFalse(((JdbcConnection) conn).isEnforceJoinOrder());
assertTrue(((JdbcConnection) conn).isDistributedJoins());
assertFalse(((JdbcConnection) conn).isCollocatedQuery());
assertFalse(((JdbcConnection) conn).skipReducerOnUpdate());
}

try (final Connection conn = DriverManager.getConnection(CFG_URL_PREFIX + "collocated=true@"
+ configURL())) {
assertFalse(((JdbcConnection)conn).isEnforceJoinOrder());
assertFalse(((JdbcConnection)conn).isDistributedJoins());
assertTrue(((JdbcConnection)conn).isCollocatedQuery());
assertFalse(((JdbcConnection)conn).skipReducerOnUpdate());
+ configURL())) {
assertFalse(((JdbcConnection) conn).isEnforceJoinOrder());
assertFalse(((JdbcConnection) conn).isDistributedJoins());
assertTrue(((JdbcConnection) conn).isCollocatedQuery());
assertFalse(((JdbcConnection) conn).skipReducerOnUpdate());
}

try (final Connection conn = DriverManager.getConnection(CFG_URL_PREFIX + "skipReducerOnUpdate=true@"
+ configURL())) {
assertFalse(((JdbcConnection)conn).isEnforceJoinOrder());
assertFalse(((JdbcConnection)conn).isDistributedJoins());
assertFalse(((JdbcConnection)conn).isCollocatedQuery());
assertTrue(((JdbcConnection)conn).skipReducerOnUpdate());
+ configURL())) {
assertFalse(((JdbcConnection) conn).isEnforceJoinOrder());
assertFalse(((JdbcConnection) conn).isDistributedJoins());
assertFalse(((JdbcConnection) conn).isCollocatedQuery());
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";

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

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

GridTestUtils.assertThrows(
log,
new Callable<Object>() {
@Override
public Object call() throws Exception {
try (Connection conn = DriverManager.getConnection(url)) {
return conn;
}
}
},
SQLException.class,
null
);
}
}
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";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems also need to cover this flag usage in tests ?


/**
* 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
Loading
Loading