# GHSA-mggx-p7jf-jgw4: jdbi3-freemarker Vulnerable to Improper Neutralization of Special Elements Used in FreeMarker Template Engine

> Attackers can execute arbitrary code via FreeMarker template injection in jdbi3-freemarker due to unrestricted class instantiation.

- **Severity:** Not scored
- **Exploited in the wild:** Not known
- **Login needed:** Not stated
- **Attackable:** Over the internet
- **Victim action needed:** Not needed
- **Fix available:** Yes
- **Published:** 5 May 2026

## What is GHSA-mggx-p7jf-jgw4?

GHSA-mggx-p7jf-jgw4 is a code injection vulnerability in org.jdbi:jdbi3-freemarker, published on 5 May 2026. It can be exploited over the network, with no action from the victim.

### Official description

# 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:
```java
// 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");
}
```

```java
// 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()`:
```java
// 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-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`.

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.
```java
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.
```bash
# 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

## Which versions are affected by GHSA-mggx-p7jf-jgw4?

2 open source packages ship an affected version, including org.jdbi:jdbi3-freemarker.


| Package | Ecosystem | Affected | Fixed in |
| --- | --- | --- | --- |
| `org.jdbi:jdbi3-freemarker` | Maven | All versions | 3.53.0 |
| `org.jdbi:jdbi3-freemarker` | Maven | All versions | 3.53.0 |

## How do I fix GHSA-mggx-p7jf-jgw4?

Upgrade org.jdbi:jdbi3-freemarker to 3.53.0 or later.

## Is GHSA-mggx-p7jf-jgw4 being exploited?

There is no known exploitation of GHSA-mggx-p7jf-jgw4 in the wild and no public exploit code.

## Fix prompt for AI coding agents

```text
Check this codebase for GHSA-mggx-p7jf-jgw4 and fix it if it is affected.

What it is: GHSA-mggx-p7jf-jgw4 is a code injection vulnerability in org.jdbi:jdbi3-freemarker, published on 5 May 2026. It can be exploited over the network, with no action from the victim.
Exploitation: There is no known exploitation of GHSA-mggx-p7jf-jgw4 in the wild and no public exploit code.

Affected packages and fixed versions:
- org.jdbi:jdbi3-freemarker (Maven): affected all versions; fixed in 3.53.0
- org.jdbi:jdbi3-freemarker (Maven): affected all versions; fixed in 3.53.0

Task:
1. Search the dependency manifests and lockfiles (for example package.json, package-lock.json, requirements.txt, poetry.lock, uv.lock, pom.xml, build.gradle, go.mod, Cargo.lock, Gemfile.lock) and any Dockerfiles for the affected packages, including transitive dependencies.
2. If an affected version is in use, upgrade it to the fixed version for its release line with the project's package manager, so the lockfile is regenerated. Prefer the smallest upgrade that fixes it.
3. If there is no fix, or the upgrade would break the build, do not guess: explain the risk and the safest workaround instead.
4. Run the tests and summarise what changed and why.
5. If nothing in the project is affected, say so and change nothing.

Reference: https://insights.sbomify.com/GHSA-mggx-p7jf-jgw4/ (Markdown: https://insights.sbomify.com/GHSA-mggx-p7jf-jgw4/index.md)
```

## Sources

- OSV: https://osv.dev/vulnerability/GHSA-mggx-p7jf-jgw4

---
From sbomify Insights: https://insights.sbomify.com/GHSA-mggx-p7jf-jgw4/
Summaries are generated and may contain mistakes; the official description above is the record.
