
POC for CVE-2026-39816 which allows NiFi users without execute code permissions to run arbitrary scripts
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.
@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:
// nifi-extension-bundles/nifi-graph-bundle/nifi-other-graph-services/
// src/main/java/org/apache/nifi/graph/TinkerpopClientService.java:451
protected Map<String, String> bytecodeSubmission(
String s, Map<String, Object> 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.
A NiFi 2.8.0 server is exploitable when both of the following are true:
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.
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.
Arbitrary code execution as the NiFi service account. From there:
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.
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.
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 + 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 , with raw-mode stdio bridging and live window
resizing — operator gets a real terminal inside the NiFi container.
Prerequisites: Docker, Python 3.10+, and uv.
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:
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 <host-bridge-ip> or add
extra_hosts: ["host.docker.internal:host-gateway"] to the nifi
service in setup/docker-compose.yml.
Tear down:
cd setup
./teardown.sh
ExecuteGraphQueryscript