Share
## https://sploitus.com/exploit?id=B035CC96-3748-50F6-AB87-32672F4BD58C
# Apache NiFi CVE-2026-39816 POC

Proof-of-concept demonstration for CVE-2026-39816, an `EXECUTE_CODE`
permission bypass in Apache NiFi 2.8.0 that lets a flow designer who has
been deliberately denied code-execution privileges run arbitrary Groovy
in the NiFi JVM via the graph bundle's `ExecuteGraphQuery` processor.

- Fixed in NiFi 2.9.0
- Discovered by ZeroPath -- full technical write up available on the ZeroPath website. 


## Vulnerability

### Missing `@Restricted` annotation on TinkerpopClientService (CVE-2026-39816, CWE-95)

NiFi gates code-executing processors behind the `EXECUTE_CODE` permission
via the `@Restricted(requiredPermission = RequiredPermission.EXECUTE_CODE)`
annotation. All 16 dedicated scripting components in NiFi โ€”
`ExecuteScript`, `InvokeScriptedProcessor`, `ExecuteGroovyScript`,
`ScriptedTransformRecord`, etc. โ€” carry this annotation, so a flow
designer must be granted the explicit `EXECUTE_CODE` permission before
the authorization layer will let them create or configure these
processors.

The graph bundle (`nifi-graph-nar` + `nifi-other-graph-services-nar`)
ships a third code-execution path that is missing this annotation:

```java
// nifi-extension-bundles/nifi-graph-bundle/nifi-other-graph-services/
//   src/main/java/org/apache/nifi/graph/TinkerpopClientService.java:451
protected Map bytecodeSubmission(
        String s, Map map, GraphQueryResultCallback cb) {
    ...
    compiled = groovyShell.parse(s);     // line 463 โ€” compile attacker string
    compiledCode.put(s, compiled);
    ...
    compiled.setBinding(bindings);
    Object result = compiled.run();      // line 477 โ€” execute in NiFi JVM
```

The `s` parameter is the query string from the `ExecuteGraphQuery`
processor's "Graph Query" property, passed through unchanged. There is
no sanitization, no allowlist, and no sandbox โ€” any valid Groovy is
compiled and executed with the full privileges of the NiFi process.

Neither `TinkerpopClientService` nor the `ExecuteGraphQuery` /
`ExecuteGraphQueryRecord` processors that drive it carry the
`@Restricted` annotation. NiFi's authorization layer therefore treats
them as ordinary components, and a flow designer who has been
explicitly denied `EXECUTE_CODE` can still create them.

### When is a server exploitable?

A NiFi 2.8.0 server is exploitable when **both** of the following are
true:

1. **The optional graph bundle is installed.** Specifically the
   `nifi-other-graph-services-nar` NAR โ€” this is the bundle that ships
   `TinkerpopClientService`. Servers without this NAR are not affected.

2. **At least one user has flow-designer-style permissions
   (`read`/`write` on a process group and on `/controller`) without
   `EXECUTE_CODE`.** This is a common policy shape: an organization
   wants users to be able to build and modify pipelines, but not run
   arbitrary code. The `EXECUTE_CODE` permission is precisely the
   security boundary that policy is meant to enforce.

If both conditions hold, the user can configure a
`TinkerpopClientService` in "ByteCode Submission" mode, point it at any
reachable Gremlin server (the Gremlin server is only used for service
initialization โ€” execution happens locally), and create an
`ExecuteGraphQuery` processor with Groovy code in the "Graph Query"
property. Starting the processor compiles and runs the Groovy in the
NiFi JVM.

### What can an attacker do?

Arbitrary code execution as the NiFi service account. From there:

- Read and modify everything on the NiFi host's filesystem, including
  the keystore, sensitive properties key, and any flow-encrypted secrets.
- Reach every internal system NiFi can reach. NiFi typically sits next
  to data-warehouse, message-bus, object-storage, and database
  credentials in its Parameter Contexts and Controller Services โ€” the
  Groovy payload can read all of them and pivot.
- Establish persistence by writing to `flow.json.gz`, dropping a NAR
  into the autoload directory, or modifying a controller service.

The exploit bypasses the `EXECUTE_CODE` permission, so this works even
when the operator has explicitly removed code-execution rights from
the user โ€” exactly the scenario the permission exists to prevent.

### POC scope

This repository ships **one** POC, covering the most direct trigger:
attacker-controlled Groovy placed directly in the `Graph Query`
processor property. No upstream connection, no Expression Language,
no FlowFile content needed โ€” just the processor running on its own
timer.

Two additional code paths exist (FlowFile body content used as the
query when `Graph Query` is empty, and Expression Language
interpolation of attacker-influenced FlowFile attributes into the
query template). They are not demonstrated here because they reach
the same sink with extra preconditions; the direct path is sufficient
to prove the `EXECUTE_CODE` bypass.


## Repository contents

- **`setup/`** โ€” Docker Compose environment. Brings up a NiFi 2.8.0
  instance (official `apache/nifi:2.8.0` image) with the graph bundle
  NARs autoloaded, an LDAP server providing two test users (`admin`
  and `flow_designer`), and a Gremlin server for the
  `TinkerpopClientService` to point at. `setup.sh` bootstraps the
  policies so that `flow_designer` has flow-editing rights but
  **not** `EXECUTE_CODE`, then verifies the configuration.

- **`pocs/flow_designer_groovy_rce.py`** โ€” Self-contained exploit.
  Authenticates as `flow_designer`, demonstrates that `ExecuteScript`
  is denied (the `EXECUTE_CODE` gate is working), then creates a
  `TinkerpopClientService` + `ExecuteGraphQuery` with a Groovy
  payload that spawns a bash reverse shell to a listener the POC
  starts locally. Once the connection is established, the POC
  upgrades the shell to a fully interactive PTY-backed bash via
  util-linux `script`, with raw-mode stdio bridging and live window
  resizing โ€” operator gets a real terminal inside the NiFi container.


## Instructions

Prerequisites: Docker, Python 3.10+, and
[uv](https://docs.astral.sh/uv/getting-started/installation/).

```bash
cd setup
./setup.sh
```

The first run downloads the graph-bundle NARs from Maven Central and
pulls Docker images (~2-3 min). When setup completes it prints the
ready-to-paste POC invocation.

Run the POC:

```bash
uv run --no-project --with requests \
    pocs/flow_designer_groovy_rce.py \
    --base-url https://localhost:8443 \
    --username flow_designer \
    --password 'flowDesigner123!' \
    --gremlin-host gremlin-server
```

The POC drops the operator into an interactive bash session inside the
NiFi container running as the `nifi` service account. Type `exit` or
press `Ctrl-D` to disconnect โ€” the POC will then clean up the processor
and controller service it created.

On Linux Docker (where `host.docker.internal` does not resolve by
default), pass `--shell-host ` or add
`extra_hosts: ["host.docker.internal:host-gateway"]` to the `nifi`
service in `setup/docker-compose.yml`.

Tear down:

```bash
cd setup
./teardown.sh
```