Skip to content

CVE-2026-49869: Verified Repro With Script Download

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 Variant found 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://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
Variants tested

Alternate unauthenticated RCE trigger on Kestra OSS v1.0.44 via the AuthenticationFilter isOpenUrl branch (startsWith on the production-default open-url /api/v1/main/executions/webhook/) + ExecutionController /webhook endpoint. The CREATE step reuses the /configs suffix bypass (isConfigEndpoint); the TRIGGER step uses…

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.

Variant Analysis & Alternative Triggers for CVE-2026-49869

Versions: versions (as tested):Fixed: (creation blocked, no RCE; open webhook latent): v1.0.45

A distinct alternate trigger for the same root-cause class was confirmed on the vulnerable Kestra OSS v1.0.44: unauthenticated RCE via the AuthenticationFilter isOpenUrl whitelist branch (request.getPath().startsWith(openUrl)), which the fix commit 28ff533d8 did not modify. The parent repro exploited the isConfigEndpoint branch (endsWith("/configs")) for both flow creation and trigger (/api/v1/main/executions/trigger/configs/configs). This variant reuses the /configs suffix bypass only for flow creation, then triggers the flow through the open webhook endpoint GET /api/v1/main/executions/webhook/configs/configs/<key> — a different whitelist branch and a different ExecutionController endpoint (/webhook vs /trigger), where the trigger path ends with the attacker-chosen webhook key, not /configs. On the fixed v1.0.45, the /configs create-bypass is closed (401), so the attacker can no longer plant the flow and no RCE occurs; however the open webhook trigger path itself remains open (404 "Flow not found", not 401), proving the isOpenUrl branch is untouched by the fix and persists on the latest v1.0.49. The open webhook is documented behavior (webhook key = secret), so this is reported as an alternate trigger + defense-in-depth gap, not a bypass.

Fix Coverage / Assumptions

Fix 28ff533d8 ("fix(auth): potential authentication bypass in the authentication filter", GHSA-5vc5-wxxq-3fjx) modifies only the isConfigEndpoint branch of AuthenticationFilter.doFilter:

  • endsWith("/configs")"/api/v1/configs".equals(normalizePath(path)) (exact).
  • endsWith("/basicAuth") → regex /api/v1(/[^/]+)?/basicAuth (still gated by !isBasicAuthInitialized()).
  • endsWith("/basicAuthValidationErrors") → exact equality (still setup-gated).
  • New normalizePath collapses //.

Invariants the fix relies on:

  • A. Only isConfigEndpoint needed hardening — the other two open branches in the same filter (isOpenUrl, isManagementEndpoint) were not touched.
  • B. Filter/router path consistency after slash-normalization (exact equality cannot diverge from routing).
  • C. basicAuth branch is setup-only (!isBasicAuthInitialized() gate) and the regex matches only the real /{tenant}/basicAuth setup route.
  • D. Flow CREATION is the gate — once creation requires auth, an unauthenticated attacker cannot plant a malicious flow, so unauthenticated RCE is prevented.

What the fix does NOT cover: the isOpenUrl branch (request.getPath().startsWith(s) for each kestra.server.basic-auth.open-urls entry) is the same over-permissive prefix string-match class as the original suffix bug, and the fix did not address it. The production-default open-urls include /api/v1/main/executions/webhook/, so the webhook execution endpoint is unauthenticated on every version including fixed/latest. Assumption D still holds on the fixed version (creation is blocked), but the trigger layer provides no defense-in-depth: any future re-bypass of flow creation would immediately expose unauthenticated RCE via the open webhook, which the fix does not protect against.

Variant / Alternate Trigger

Entry points (unauthenticated HTTP):

  1. POST /api/v1/main/flows/configs?delete=false — create a flow with namespace=configs, id=configs, a io.kestra.plugin.core.trigger.Webhook trigger (attacker-chosen key), and a io.kestra.plugin.scripts.shell.Commands task. Path ends with /configsisConfigEndpoint bypass (same create step as parent repro).
  2. GET /api/v1/main/executions/webhook/configs/configs/<key> — trigger the flow via the open-url webhook. Path starts with /api/v1/main/executions/webhook/isOpenUrl bypass (path ends with /<key>, not /configs). Reaches ExecutionController.triggerExecutionByGetWebhookwebhook() → flow shell task → RCE.

Code path:

  • AuthenticationFilter.doFilterisOpenUrl (startsWith) true → chain.proceed (no credential check). File: webserver/src/main/java/io/kestra/webserver/filter/AuthenticationFilter.java:66-72.
  • ExecutionController.triggerExecutionByGetWebhook / webhook(...)flowRepository.findById(...)webhook.evaluate(request, flow)executionQueue.emit(result) → worker runs the shell task. File: webserver/src/main/java/io/kestra/webserver/controllers/api/ExecutionController.java:493-620.
  • Default open-urls whitelisting /api/v1/main/executions/webhook/: cli/src/main/resources/application.yml:166-173.
  • Webhook trigger (key = secret): core/src/main/java/io/kestra/plugin/core/trigger/Webhook.java:102-120.

Rule-out matrix (true bypass on fixed, normal operation):

  • /configs exact-match edge cases on fixed: /api/v1//configs404 (whitelisted by normalizePath but unroutable → harmless); /api/v1/configs/ (trailing slash) → 401; /api/v1/main/flows%2Fconfigs (encoded slash) → 401. No bypass.

  • basicAuth regex (setup state, !isBasicAuthInitialized()): GET /api/v1/flows/basicAuth200 (whitelisted; routes via aliasing to FlowController /{namespace} read of namespace basicAuth → negligible); GET /api/v1/main/namespace/basicAuth (two segments) → 401 (fix's own test case); POST /api/v1/flows/basicAuth422 (routes to MiscController createBasicAuth setup endpoint, not FlowController create). No normal-operation bypass, no RCE.

  • Package/component: io.kestra.webserver.filter.AuthenticationFilter (isOpenUrl branch) + io.kestra.webserver.controllers.api.ExecutionController (webhook endpoints); default open-urls in cli/.../application.yml.

  • Affected versions (as tested):

    • Vulnerable (alternate-trigger RCE confirmed): v1.0.44 (kestra/kestra:v1.0.44, git 3cf9c7f5cc).
    • Fixed (creation blocked, no RCE; open webhook latent): v1.0.45 (kestra/kestra:v1.0.45, git 9225d093663; fix 28ff533d8 is an ancestor).
    • Latest 1.0.x checked: v1.0.49isOpenUrl + default open-urls are byte-identical to the fixed version (gap persists).
  • Risk level: Critical on the vulnerable version (unauthenticated RCE, identical to the parent). On the fixed version the alternate-trigger chain is broken at the creation step; the residual open-webhook trigger is documented behavior but represents a defense-in-depth gap (no trigger-layer protection).

Impact Parity

  • Disclosed/claimed maximum impact (parent): Unauthenticated remote code execution (critical).
  • Reproduced impact from this variant run:
    • Vulnerable v1.0.44: full — unauthenticated OS command execution in the Kestra worker (marker file written by the attacker echo command, owned by kestra:kestra), via a different trigger path than the parent repro.
    • Fixed v1.0.45: none — no RCE (creation blocked at 401); open webhook returns 404.
  • Parity (vulnerable): full. Parity (fixed): none (not a bypass).
  • Not demonstrated: RCE on the fixed version (by design — the fix closes creation); the latent open-webhook trigger surface is documented, not exploited.

Root Cause

AuthenticationFilter implements three independent open-path whitelist branches (isConfigEndpoint, isOpenUrl, isManagementEndpoint); any one being true skips Basic-Auth and calls chain.proceed. The original CVE abused the isConfigEndpoint suffix test. The fix hardened only isConfigEndpoint (exact match + normalizePath). The isOpenUrl branch uses a prefix test (startsWith) over the production-default open-urls, which include /api/v1/main/executions/webhook/; that prefix matches the ExecutionController webhook trigger routes, leaving them unauthenticated on every version. Because the webhook trigger is a documented public trigger (key = secret), the open webhook is not itself a flaw — but it is the same over-permissive string-match whitelist class in the same filter, and it is the mechanism by which an attacker-created flow (planted via the /configs create-bypass on the vulnerable version) is triggered unauthenticated through a different endpoint than the parent repro. Fix commit: 28ff533d85e6c977e209f90e87e8523d0f8888eb.

Reproduction Steps

  1. Script: bundle/vuln_variant/reproduction_steps.sh.
  2. What it does (idempotent, runs both versions side by side from the official Docker images, with the production-default open-urls so the webhook is open as in real deployments):
    • Vulnerable v1.0.44 (basic auth configured = normal operation):
      1. Control GET /api/v1/main/flows/configs/configs/revisions (no creds) → 401.
      2. Create the webhook-equipped malicious flow via the /configs bypass POST /api/v1/main/flows/configs (no creds) → 200.
      3. Variant trigger GET /api/v1/main/executions/webhook/configs/configs/<key> (no creds; path ends with /<key>, not /configs) → 200 and a marker file written by the attacker shell command.
    • Fixed v1.0.45 (normal operation):
      1. Create flow via /configs bypass → 401 (fix blocks creation).
      2. Open webhook path check GET /api/v1/main/executions/webhook/configs/configs/<key>404 (not 401) — isOpenUrl branch still open, flow not found.
      3. /configs exact-match edge cases (//configs, trailing slash, %2F) → 404/401 (no bypass).
    • Fixed v1.0.45 (setup state, no basic auth): basicAuth regex rule-out.
  3. Expected evidence: vulnerable webhook trigger returns 200 with an execution whose state reaches SUCCESS and a marker VARIANT_KESTRA_RCE_<token> owned by kestra:kestra; fixed returns 401 for creation and 404 for the webhook (no marker).

Evidence

  • bundle/logs/variant_results_summary.txt — annotated HTTP status codes for every request on both versions + verdict flags.
  • bundle/logs/variant_vuln_marker.txtls -l + cat of the marker file (-rw-r--r-- 1 kestra kestra ... VARIANT_KESTRA_RCE_<token>).
  • bundle/logs/variant_vuln_trigger_resp.txt200 webhook response body: an execution with flowId:configs, namespace:configs, trigger:{id:webhook, type:io.kestra.plugin.core.trigger.Webhook}, state.current:SUCCESS.
  • bundle/logs/variant_vuln_create_flow_resp.txt200 flow-create response.
  • bundle/logs/variant_fixed_webhook_resp.txt404 "Not Found: Flow not found" (not 401), confirming isOpenUrl bypassed auth on the fixed version.
  • bundle/logs/variant_fixed_create_flow_resp.txt401 (creation blocked by fix).
  • bundle/logs/variant_fixed_marker.txt — no marker on fixed.
  • bundle/logs/variant_{vuln,fixed}_container.log — server startup logs.
  • bundle/vuln_variant/runtime_manifest.json — structured runtime evidence.
  • bundle/vuln_variant/source_identity.json — exact tested commits/versions.

Key excerpt (vulnerable, unauthenticated alternate trigger):

[vuln] POST /api/v1/main/flows/configs (no creds, /configs suffix, create webhook flow) -> 200
[vuln] GET /api/v1/main/executions/webhook/configs/configs/variantSecretKey2026 (no creds, OPEN-URL webhook, ends with /<key> NOT /configs) -> 200
[vuln] ALTERNATE-TRIGGER RCE CONFIRMED via open-webhook (isOpenUrl branch): marker=VARIANT_KESTRA_RCE_<token>

Key excerpt (fixed, negative):

[fixed] POST /api/v1/main/flows/configs (no creds, create) -> 401
[fixed] GET /api/v1/main/executions/webhook/configs/configs/variantSecretKey2026 (no creds) -> 404
[fixed] no marker file created (expected: creation blocked)

Environment: Official Docker images kestra/kestra:v1.0.44 and :v1.0.45 (eclipse-temurin 21 JRE, 844 bundled plugins). Standalone H2 in-memory. Basic Auth enabled (normal operation) and a separate setup-state instance (no basic auth). Production-default open-urls applied. All HTTP requests issued from inside the running Kestra container (docker exec ... curl localhost:8080) for network-topology independence. The OS command executed as the kestra service account.

Recommendations / Next Steps

  1. The fix is correct and complete for the disclosed CVE (unauthenticated RCE via the /configs suffix in normal operation). No change needed to the isConfigEndpoint hardening.
  2. Defense in depth at the trigger layer: recognize that AuthenticationFilter has multiple whitelist branches and that isOpenUrl (startsWith) leaves the webhook execution endpoint unauthenticated by default. The mitigation should NOT rely solely on blocking flow creation:
    • Treat webhook keys as critical secrets (enforce minimum entropy / random generation; the Webhook schema already warns about this).
    • Consider gating webhook-triggered execution of flows that contain code-execution tasks (shell/scripts) behind an additional check, or require webhook flows to be explicitly marked as "publicly triggerable" by an authenticated admin.
    • Optionally tighten isOpenUrl to exact/route-aware matching instead of raw startsWith, so a misconfigured or future open-url prefix cannot accidentally expose more than the intended webhook route.
  3. Monitoring: alert on unauthenticated POST/GET to /api/v1/**/executions/webhook/** for flows containing shell/script tasks, and on flows whose id/namespace collide with whitelisted suffixes (configs, basicAuth, basicAuthValidationErrors).
  4. Regression tests: extend the fix's test suite to assert that the isOpenUrl branch cannot be used to reach non-webhook authenticated endpoints, and that webhook triggering of a code-execution flow is auditable.

Additional Notes

  • Idempotency: the script removes prior containers, uses a run-unique marker token, and writes fresh evidence each run. Verified to run twice consecutively with identical results (exit 1 = alternate trigger on vulnerable, not a bypass on fixed).
  • Bypass vs alternate trigger: this is an alternate trigger (vulnerable only), not a bypass. On the fixed version the attacker cannot create the flow (401), so the open webhook has nothing to trigger (404, no RCE).
  • Documented-behavior caveat: the webhook endpoint being unauthenticated is documented and accepted (the webhook key is the secret). The variant's CVE-relevance comes from pairing the open webhook with the unauthenticated flow CREATE (the /configs bypass) on the vulnerable version, exercising a different filter branch and endpoint than the parent repro. The security-relevant takeaway for the fix is the untouched isOpenUrl branch and the resulting lack of trigger-layer defense-in-depth.
  • Scope of proof: api_remote surface and code_execution impact are both matched on the vulnerable version via the alternate path; on the fixed version the chain is blocked at creation (no RCE), consistent with the fix's intent.

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.