Skip to content

CVE-2026-49869: Verified Reproduction

CVE-2026-49869: Kestra unauthenticated RCE via AuthenticationFilter path bypass

CVE-2026-49869 is verified against the affected target. Vulnerability class: RCE. This critical reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00247.

REPRO-2026-00247 RCE Jul 6, 2026 CVE entry .txt
Severity
CRITICAL
CVSS
10.0
Confidence
HIGH
Reproduced in
26m 25s
Tool calls
257
Spend
$4.30
01 · Overview

What Is CVE-2026-49869?

CVE-2026-49869 is a critical unauthenticated remote code execution vulnerability in Kestra OSS, caused by an authentication-filter bypass that lets attackers reach internal APIs and trigger command execution. Pruva reproduced it (reproduction REPRO-2026-00247).

02 · Severity & CVSS

CVE-2026-49869 Severity & CVSS Score

CVE-2026-49869 is rated critical severity, with a CVSS base score of 10.0 out of 10.

CRITICAL threat level
10.0 / 10 CVSS base
Weakness CWE-78 — Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')

Critical — the most severe class — typically remotely exploitable with severe impact. Treat as an emergency.

How to Reproduce CVE-2026-49869

$ pruva-verify REPRO-2026-00247
or curl -O https://www.pruva.dev/api/v1/reproductions/REPRO-2026-00247/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh
Run in a VM or disposable container. This exploits a real vulnerability.
06 · Proof of Reproduction

Proof of Reproduction for CVE-2026-49869

Remote code execution — reproduced
  • reached the target end-to-end
  • full exploit chain demonstrated
  • on the real production code path
  • high confidence
  • the upstream fix blocks the same trigger
Trigger

Unauthenticated HTTP requests to /api/v1/main/flows/configs and /api/v1/main/executions/trigger/configs/configs (paths ending in /configs) plus an attacker-supplied flow YAML containing arbitrary shell commands.

Attack chain
  1. AuthenticationFilter.doFilter uses request.getPath().endsWith("/configs") to whitelist the public config endpoint; setting a flow namespace and id to the literal 'configs' makes POST /api/v1/main/flows/configs and POST /api/v1/main/executions/trigger/configs/configs end with '/configs', bypassing Basic Auth. The created flow's shell task then runs an arbitrary OS command in the Kestra worker.
Runnable proof: reproduction_steps.sh
Captured evidence: fixed container
How the agent worked 600 events · 257 tool calls · 26 min
26 minDuration
257Tool calls
142Reasoning steps
600Events
3Dead-ends
Agent activity over 26 min
Support
18
Hypothesis
2
Repro
355
Judge
35
Variant
186
0:0026:25

Root Cause and Exploit Chain for CVE-2026-49869

Versions: Kestra OSS < 1.0.45 and < 1.3.21 (the 1.3.x line before 1.3.21). The Enterprise Edition uses Micronaut Security (@Requires(property = "micronaut.security.enabled", notEquals = "true") excludes EE) and is not affected by this OSS filter.

Kestra OSS (before 1.0.45 and 1.3.21) ships an AuthenticationFilter that whitelists the public configuration endpoint with a pure path-suffix test:

boolean isConfigEndpoint = request.getPath().endsWith("/configs") || ...

Because the check only tests whether the request path ends with the literal /configs segment, any /api/v1/** request whose final path segment is configs bypasses Basic Authentication — including requests where configs is simply the value of a path variable (a flow namespace or flow id). An unauthenticated remote attacker can therefore reach arbitrary authenticated controllers. By creating a flow whose namespace and id are both the literal configs, an attacker makes both the flow-create endpoint (POST /api/v1/main/flows/configs) and the execution-trigger endpoint (POST /api/v1/main/executions/trigger/configs/configs) end with /configs, bypassing the filter end-to-end and achieving unauthenticated remote code execution through a shell task inside the flow.

  • Package/component affected: webserver/src/main/java/io/kestra/webserver/filter/AuthenticationFilter.java (OSS Basic-Auth filter), and every controller under /api/v1/** that the filter protects.
  • Affected versions: Kestra OSS < 1.0.45 and < 1.3.21 (the 1.3.x line before 1.3.21). The Enterprise Edition uses Micronaut Security (@Requires(property = "micronaut.security.enabled", notEquals = "true") excludes EE) and is not affected by this OSS filter.
  • Risk level: Critical. Unauthenticated, remotely reachable, and trivially exploitable. Consequences are arbitrary OS command execution on the Kestra worker (as the service account), plus unauthenticated read/write of any flow, execution, log, or namespace resource exposed under /api/v1/**.

Impact Parity

  • Disclosed/claimed maximum impact: Unauthenticated remote code execution (severity: critical).
  • Reproduced impact from this run: Unauthenticated remote code execution — an attacker-controlled shell command ran inside the Kestra worker and wrote a marker file as the kestra service user, reached entirely through unauthenticated HTTP requests to the real running Kestra webserver.
  • Parity: full.
  • Not demonstrated: N/A — the full claimed impact (unauthenticated RCE via the API) was demonstrated end-to-end on the real product.

Root Cause

AuthenticationFilter.doFilter runs for every request matching @Filter("/api/v1/**") (when kestra.server-type is WEBSERVER|STANDALONE and Micronaut Security is not enabled, i.e. OSS). It computes:

boolean isConfigEndpoint = request.getPath().endsWith("/configs")
    || ((request.getPath().endsWith("/basicAuth") || request.getPath().endsWith("/basicAuthValidationErrors"))
        && !basicAuthService.isBasicAuthInitialized());

and, if isConfigEndpoint (or an open-URL / management endpoint) is true, calls chain.proceed(request) without verifying credentials. The legitimate public endpoint is GET /api/v1/configs (MiscController). The intended check was "is this the configs endpoint", but endsWith("/configs") accepts any path that merely terminates in that segment.

The Kestra API is tenant-prefixed: FlowController is mapped at /api/v1/{tenant}/flows and ExecutionController at /api/v1/{tenant}/executions (OSS TenantAliasingRooter rewrites /api/v1/<x> to /api/v1/main/<x> when no route matches directly). So a flow whose namespace=configs and id=configs makes the create and trigger URIs end in /configs:

Action URI Last segment Filter decision
Create flow POST /api/v1/main/flows/configs configs bypass → chain.proceed
Trigger flow POST /api/v1/main/executions/trigger/configs/configs configs bypass → chain.proceed

Both reach the controller unauthenticated. The flow contains a io.kestra.plugin.scripts.shell.Commands task (bundled in the official image) using the io.kestra.plugin.core.runner.Process task runner, so the attacker's shell command executes directly inside the Kestra worker process.

Fix commit: 28ff533d8 ("fix(auth): potential authentication bypass in the authentication filter", GHSA-5vc5-wxxq-3fjx), released in v1.0.45 / v1.3.21. The fix replaces the suffix test with an exact, normalized match:

String normalizedPath = normalizePath(request.getPath()); // path.replaceAll("/+", "/")
boolean isConfigEndpoint = "/api/v1/configs".equals(normalizedPath)
    || ((normalizedPath.matches("/api/v1(/[^/]+)?/basicAuth")
        || "/api/v1/basicAuthValidationErrors".equals(normalizedPath))
        && !basicAuthService.isBasicAuthInitialized());

The normalizePath step (collapsing //) also closes a related double-slash evasion; the added regression tests assert that /api/v1/main/flows/namespace/configs, /api/v1/main/namespace/basicAuth, and /api/v1/main/basicAuthValidationErrors now return 401.

Reproduction Steps

  1. Script: bundle/repro/reproduction_steps.sh (self-contained, idempotent).
  2. What it does:
    1. Pulls the official Kestra images kestra/kestra:v1.0.44 (vulnerable) and kestra/kestra:v1.0.45 (fixed).
    2. Starts each image as a real server standalone instance with an in-memory H2 backend and Basic Auth enabled (admin@kestra.io / Password1) — the same AuthenticationFilter that protects production deployments.
    3. Against the vulnerable instance, with no credentials, it:
      • sends a control request to a protected path that does not end in /configs (expect 401),
      • sends the same resource via a /configs-suffixed path (expect not 401 — bypass),
      • POST /api/v1/main/flows/configs to create an arbitrary flow (id=configs, namespace=configs) containing a shell task,
      • POST /api/v1/main/executions/trigger/configs/configs?wait=true to trigger it,
      • reads the marker file the shell command wrote inside the worker container.
    4. Against the fixed instance it repeats the same unauthenticated bypass requests (expect 401 for all) and confirms no marker is produced.
    5. Writes bundle/repro/runtime_manifest.json and bundle/logs/results_summary.txt.
  3. Expected evidence of reproduction:
    • Vulnerable: control=401, bypass=404 (filter bypassed, router reached), unauth create=200, unauth trigger=200, and a marker file PWNED_KESTRA_RCE_<token> written by the attacker command as the kestra user.
    • Fixed: bypass=401, unauth create=401, unauth trigger=401, no marker file.

Evidence

  • bundle/logs/results_summary.txt — annotated HTTP status codes for every request on both versions and the final verdict flags.
  • bundle/logs/vuln_create_flow_resp.txt200 response body returning the created flow (revision 1) with the attacker's shell task.
  • bundle/logs/vuln_trigger_resp.txt200 response body returning the execution whose task run reached SUCCESS.
  • bundle/logs/vuln_marker.txtls -l + cat of the marker file, owned by kestra:kestra, containing the run-unique PWNED_KESTRA_RCE_<token> string produced by the injected echo command.
  • bundle/logs/fixed_bypass_resp.txt, bundle/logs/fixed_create_flow_resp.txt, bundle/logs/fixed_trigger_resp.txt401 responses on the fixed version.
  • bundle/logs/fixed_marker.txt — confirms no marker file exists on the fixed version.
  • bundle/logs/vuln_container.log, bundle/logs/fixed_container.log — server startup logs (844 plugins registered; standalone server running).
  • bundle/repro/malicious_flow.yaml — the exact flow source submitted unauthenticated.
  • bundle/repro/runtime_manifest.json — structured runtime evidence (entrypoint_kind=api_remote, target_path_reached=true, rce_observed=true, fixed_blocks_bypass=true).

Key excerpt (vulnerable, unauthenticated):

[vuln] GET /api/v1/main/flows/configs/configs/revisions (no creds, no /configs suffix) -> 401 (expect 401: auth enforced)
[vuln] GET /api/v1/main/flows/configs/configs (no creds, /configs suffix) -> 404 (expect NOT 401: bypass)
[vuln] POST /api/v1/main/flows/configs (no creds, create flow) -> 200 (expect 200)
[vuln] POST /api/v1/main/executions/trigger/configs/configs?wait=true (no creds, trigger) -> 200 (expect 200)
[vuln] RCE CONFIRMED: marker file written by attacker command: PWNED_KESTRA_RCE_<token>

Key excerpt (fixed, negative control):

[fixed] GET .../flows/configs/configs (no creds, /configs suffix) -> 401 (expect 401: bypass closed)
[fixed] POST .../flows/configs (no creds, create flow) -> 401 (expect 401)
[fixed] POST .../trigger/configs/configs (no creds, trigger) -> 401 (expect 401)
[fixed] no marker file created (expected)

Environment: Official Docker images kestra/kestra:v1.0.44 and :v1.0.45 (eclipse-temurin 21 JRE, 844 bundled plugins). Standalone topology with in-memory H2 queue/repository and local storage. Basic Auth enabled. All HTTP requests were issued from inside the running Kestra container (docker exec <container> curl http://localhost:8080/...) so the proof is independent of the host/sandbox network topology. The OS command executed as the kestra service account (uid 1000).

Recommendations / Next Steps

  • Upgrade to Kestra OSS >= 1.0.45 or >= 1.3.21 immediately. The fix replaces the suffix whitelist with an exact, slash-normalized equality check.
  • Defense in depth: do not rely on path-string suffix matching for authz decisions; match the exact public route(s) and prefer the framework's security interceptor / RBAC layer (Micronaut Security in EE) over hand-rolled filters.
  • Restrict egress / sandbox task runners so that even if a filter bypass occurs, a compromised worker cannot trivially exfiltrate data or pivot. Consider running script tasks via the Docker task runner in an isolated, egress-controlled network rather than the in-process Process runner.
  • Monitoring: alert on unauthenticated POST activity against /api/v1/**/flows/* and /api/v1/**/executions/trigger/*, and on flows whose id/namespace collide with whitelisted suffixes (configs, basicAuth, basicAuthValidationErrors).
  • Testing: add integration tests that assert every whitelisted "open" path is matched exactly (not by suffix) and that arbitrary /configs-suffixed protected paths return 401. The upstream regression tests added in the fix commit (configEndpointShouldBeCheckedExactly, basicAuthEndpointShouldBeCheckedExactly, basicAuthValidationErrorsEndpointShouldBeCheckedExactly) are good references.

Additional Notes

  • Idempotency: the script removes any prior kestra-vuln/kestra-fixed containers before starting, uses a run-unique marker token (PWNED_KESTRA_RCE_<epoch>_<pid>), and writes fresh evidence files each run. It has been verified to pass end-to-end (exit 0) on consecutive runs.
  • Scope of proof: the claim surface (api_remote) and impact (code_execution) are both matched: the proof is a real unauthenticated HTTP request to the real running Kestra API that results in an attacker-controlled OS command executing in the worker.
  • Why the marker is read via docker exec: the sandbox executing the script may itself be a container on a different Docker network than the Kestra container, so reaching 127.0.0.1:8080 from the script is not reliable. Issuing requests from inside the Kestra container (docker exec ... curl localhost:8080) makes the proof independent of the host/sandbox network topology while still exercising the real HTTP API boundary.
  • Limitations: the reproduction uses the bundled io.kestra.plugin.core.runner.Process task runner so that no Docker-in-Docker / Docker socket is required; a production deployment using the default Docker task runner would execute the same attacker command inside an isolated container, with the same RCE impact. The tutorialFlows.enabled=false setting only suppresses sample-blueprint loading and does not affect the vulnerability.

CVE-2026-49869 Reproduction Transcript

The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired.

Full session Replay every step — scrub the timeline or play it back.

Event 1/40
0:000:57
0:00
session startedaccounts/fireworks/routers/glm-5p2-fast · CVE-2026-49869 · REPRO-20
0:02
0:04
web search
0:05
web search
0:07
0:08
0:09
0:16
0:17
web search
0:19
web search
0:31
0:31
extract_facts
no facts extracted
0:32
0:32
0:32
supportrepro
0:49
0:51
0:51
0:51
0:52
0:52
0:52
0:54
0:54
0:54
0:55
0:55
0:57
08 · How to Fix

How to Fix CVE-2026-49869

Coming soon

Step-by-step mitigation and hardening guidance for CVE-2026-49869 — configuration checks, workarounds where no patch exists, and how to verify you're protected — is on the way.

10 · FAQ

FAQ: CVE-2026-49869

How does the Kestra AuthenticationFilter bypass work?

An attacker creates a flow whose namespace and id are both the literal string 'configs', making both the flow-create endpoint (POST /api/v1/main/flows/configs) and the execution-trigger endpoint (POST /api/v1/main/executions/trigger/configs/configs) end with '/configs'. Both requests slip past the filter unauthenticated, letting the attacker create and trigger a flow containing a shell task and achieve remote code execution.

How severe is CVE-2026-49869?

Critical — it grants unauthenticated remote code execution against Kestra OSS builds before 1.0.45 and 1.3.21, since the affected AuthenticationFilter is only present in the OSS Basic-Auth path (the Enterprise Edition uses a separate Micronaut Security mechanism).

How can I reproduce CVE-2026-49869?

Download the verified script from this page and run it in an isolated environment against a vulnerable Kestra OSS build. It sends unauthenticated HTTP requests ending in '/configs' to create a flow and trigger its execution, demonstrating command execution via a shell task without ever authenticating.
11 · References

References for CVE-2026-49869

Authoritative sources for CVE-2026-49869 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.