## 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
```