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.
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).
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 — 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 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 Proof of Reproduction for CVE-2026-49869
- 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
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.
- 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.
reproduction_steps.sh 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
Root Cause and Exploit Chain for CVE-2026-49869
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.45and< 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
kestraservice 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
- Script:
bundle/repro/reproduction_steps.sh(self-contained, idempotent). - What it does:
- Pulls the official Kestra images
kestra/kestra:v1.0.44(vulnerable) andkestra/kestra:v1.0.45(fixed). - Starts each image as a real
server standaloneinstance with an in-memory H2 backend and Basic Auth enabled (admin@kestra.io/Password1) — the sameAuthenticationFilterthat protects production deployments. - Against the vulnerable instance, with no credentials, it:
- sends a control request to a protected path that does not end in
/configs(expect401), - sends the same resource via a
/configs-suffixed path (expect not401— bypass), POST /api/v1/main/flows/configsto create an arbitrary flow (id=configs,namespace=configs) containing a shell task,POST /api/v1/main/executions/trigger/configs/configs?wait=trueto trigger it,- reads the marker file the shell command wrote inside the worker container.
- sends a control request to a protected path that does not end in
- Against the fixed instance it repeats the same unauthenticated bypass requests (expect
401for all) and confirms no marker is produced. - Writes
bundle/repro/runtime_manifest.jsonandbundle/logs/results_summary.txt.
- Pulls the official Kestra images
- Expected evidence of reproduction:
- Vulnerable: control=
401, bypass=404(filter bypassed, router reached), unauth create=200, unauth trigger=200, and a marker filePWNED_KESTRA_RCE_<token>written by the attacker command as thekestrauser. - Fixed: bypass=
401, unauth create=401, unauth trigger=401, no marker file.
- Vulnerable: control=
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.txt—200response body returning the created flow (revision 1) with the attacker's shell task.bundle/logs/vuln_trigger_resp.txt—200response body returning the execution whose task run reachedSUCCESS.bundle/logs/vuln_marker.txt—ls -l+catof the marker file, owned bykestra:kestra, containing the run-uniquePWNED_KESTRA_RCE_<token>string produced by the injectedechocommand.bundle/logs/fixed_bypass_resp.txt,bundle/logs/fixed_create_flow_resp.txt,bundle/logs/fixed_trigger_resp.txt—401responses 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.45or>= 1.3.21immediately. 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
Processrunner. - Monitoring: alert on unauthenticated
POSTactivity against/api/v1/**/flows/*and/api/v1/**/executions/trigger/*, and on flows whoseid/namespacecollide 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 return401. 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-fixedcontainers 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 reaching127.0.0.1:8080from 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.Processtask 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. ThetutorialFlows.enabled=falsesetting only suppresses sample-blueprint loading and does not affect the vulnerability.
Variant Analysis & Alternative Triggers for CVE-2026-49869
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
normalizePathcollapses//.
Invariants the fix relies on:
- A. Only
isConfigEndpointneeded 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}/basicAuthsetup 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):
POST /api/v1/main/flows/configs?delete=false— create a flow withnamespace=configs,id=configs, aio.kestra.plugin.core.trigger.Webhooktrigger (attacker-chosenkey), and aio.kestra.plugin.scripts.shell.Commandstask. Path ends with/configs→isConfigEndpointbypass (same create step as parent repro).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/→isOpenUrlbypass (path ends with/<key>, not/configs). ReachesExecutionController.triggerExecutionByGetWebhook→webhook()→ flow shell task → RCE.
Code path:
AuthenticationFilter.doFilter→isOpenUrl(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-urlswhitelisting/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):
/configsexact-match edge cases on fixed:/api/v1//configs→ 404 (whitelisted bynormalizePathbut 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/basicAuth→ 200 (whitelisted; routes via aliasing to FlowController/{namespace}read of namespacebasicAuth→ negligible);GET /api/v1/main/namespace/basicAuth(two segments) → 401 (fix's own test case);POST /api/v1/flows/basicAuth→ 422 (routes to MiscControllercreateBasicAuthsetup endpoint, not FlowController create). No normal-operation bypass, no RCE.Package/component:
io.kestra.webserver.filter.AuthenticationFilter(isOpenUrlbranch) +io.kestra.webserver.controllers.api.ExecutionController(webhook endpoints); defaultopen-urlsincli/.../application.yml.Affected versions (as tested):
- Vulnerable (alternate-trigger RCE confirmed): v1.0.44 (
kestra/kestra:v1.0.44, git3cf9c7f5cc). - Fixed (creation blocked, no RCE; open webhook latent): v1.0.45
(
kestra/kestra:v1.0.45, git9225d093663; fix28ff533d8is an ancestor). - Latest 1.0.x checked: v1.0.49 —
isOpenUrl+ default open-urls are byte-identical to the fixed version (gap persists).
- Vulnerable (alternate-trigger RCE confirmed): v1.0.44 (
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
echocommand, owned bykestra:kestra), via a different trigger path than the parent repro. - Fixed v1.0.45: none — no RCE (creation blocked at
401); open webhook returns404.
- Vulnerable v1.0.44: full — unauthenticated OS command execution in the
Kestra worker (marker file written by the attacker
- 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
- Script:
bundle/vuln_variant/reproduction_steps.sh. - What it does (idempotent, runs both versions side by side from the official
Docker images, with the production-default
open-urlsso the webhook is open as in real deployments):- Vulnerable v1.0.44 (basic auth configured = normal operation):
- Control
GET /api/v1/main/flows/configs/configs/revisions(no creds) →401. - Create the webhook-equipped malicious flow via the
/configsbypassPOST /api/v1/main/flows/configs(no creds) →200. - Variant trigger
GET /api/v1/main/executions/webhook/configs/configs/<key>(no creds; path ends with/<key>, not/configs) →200and a marker file written by the attacker shell command.
- Control
- Fixed v1.0.45 (normal operation):
- Create flow via
/configsbypass →401(fix blocks creation). - Open webhook path check
GET /api/v1/main/executions/webhook/configs/configs/<key>→404(not401) —isOpenUrlbranch still open, flow not found. /configsexact-match edge cases (//configs, trailing slash,%2F) →404/401(no bypass).
- Create flow via
- Fixed v1.0.45 (setup state, no basic auth): basicAuth regex rule-out.
- Vulnerable v1.0.44 (basic auth configured = normal operation):
- Expected evidence: vulnerable webhook trigger returns
200with an execution whose state reachesSUCCESSand a markerVARIANT_KESTRA_RCE_<token>owned bykestra:kestra; fixed returns401for creation and404for 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.txt—ls -l+catof the marker file (-rw-r--r-- 1 kestra kestra ... VARIANT_KESTRA_RCE_<token>).bundle/logs/variant_vuln_trigger_resp.txt—200webhook response body: an execution withflowId:configs, namespace:configs, trigger:{id:webhook, type:io.kestra.plugin.core.trigger.Webhook},state.current:SUCCESS.bundle/logs/variant_vuln_create_flow_resp.txt—200flow-create response.bundle/logs/variant_fixed_webhook_resp.txt—404 "Not Found: Flow not found"(not401), confirmingisOpenUrlbypassed auth on the fixed version.bundle/logs/variant_fixed_create_flow_resp.txt—401(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
- The fix is correct and complete for the disclosed CVE (unauthenticated RCE
via the
/configssuffix in normal operation). No change needed to theisConfigEndpointhardening. - Defense in depth at the trigger layer: recognize that
AuthenticationFilterhas multiple whitelist branches and thatisOpenUrl(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
isOpenUrlto exact/route-aware matching instead of rawstartsWith, so a misconfigured or future open-url prefix cannot accidentally expose more than the intended webhook route.
- Monitoring: alert on unauthenticated
POST/GETto/api/v1/**/executions/webhook/**for flows containing shell/script tasks, and on flows whoseid/namespacecollide with whitelisted suffixes (configs,basicAuth,basicAuthValidationErrors). - Regression tests: extend the fix's test suite to assert that the
isOpenUrlbranch 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
/configsbypass) on the vulnerable version, exercising a different filter branch and endpoint than the parent repro. The security-relevant takeaway for the fix is the untouchedisOpenUrlbranch and the resulting lack of trigger-layer defense-in-depth. - Scope of proof:
api_remotesurface andcode_executionimpact 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.
Artifacts and Evidence for CVE-2026-49869
Scripts, logs, diffs, and output captured during the reproduction.
How to Fix CVE-2026-49869
FAQ: CVE-2026-49869
How does the Kestra AuthenticationFilter bypass work?
How severe is CVE-2026-49869?
How can I reproduce CVE-2026-49869?
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.