Skip to content

escape attachment filename in Content-Disposition header - #69435

Merged
potiuk merged 1 commit into
apache:mainfrom
Samin061:content-disposition-filename-escape
Jul 31, 2026
Merged

escape attachment filename in Content-Disposition header#69435
potiuk merged 1 commit into
apache:mainfrom
Samin061:content-disposition-filename-escape

Conversation

@Samin061

@Samin061 Samin061 commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

build_mime_message in airflow.utils.email and SmtpHook._build_mime_message build the attachment Content-Disposition with an f-string, so a filename containing a double quote breaks out of the quoted value and injects extra Content-Disposition parameters, letting a crafted attachment name override the filename the recipient's mail client shows or saves. Switch both to Message.add_header with a filename parameter, which lets the stdlib escape the value: ordinary names serialize exactly as before, an ASCII name with a quote is backslash-escaped inside the quoted-string, and non-ASCII names are RFC 2231 encoded. get_filename() round-trips in every case.


Was generative AI tooling used to co-author this PR?
  • Yes (please specify the tool below)

@Vamsi-klu

Copy link
Copy Markdown
Contributor

Confirmed the root cause and the fix. build_mime_message and SmtpHook._build_mime_message both built Content-Disposition via an f-string, so a filename containing a double quote (e.g. report.txt"; x-evil="1) closes the quoted value and injects an extra parameter. Switching to part.add_header("Content-Disposition", "attachment", filename=basename) hands escaping to the stdlib. Ordinary names come out byte-identical to the old output (attachment; filename="report.txt"), names with a quote get backslash-escaped inside the quotes with no injected param, and non-ASCII names are RFC 2231 encoded; get_filename() round-trips in every case. Both call sites are fixed and each has a regression test that fails against the old f-string (asserts get_filename() equals the raw name and x-evil is not in the parsed params). I also checked the imap provider hook, which showed up in a search for the same pattern, but it only parses received Content-Disposition headers, so there is no third site to fix. One nit: the description says RFC 2231-encoded, but for ASCII-with-quote inputs it is actually quoted-string backslash escaping (RFC 2231 only kicks in for non-ASCII) -- same security result, just worth correcting in the summary. Practical severity is limited since attachment filenames are usually operator/DAG-author controlled, so this is hardening more than a high-severity exploit. LGTM.

@Samin061

Samin061 commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

Good catch on the RFC 2231 wording. The quoted-string backslash escaping only crosses over into 2231 encoding for non-ASCII names, so I reworded the description to spell out both cases (plain quoted-string for the ASCII-with-quote path, 2231 for non-ASCII). Same behavior in code, just clearer in the summary. Thanks for the review.

@potiuk potiuk added the ready for maintainer review Set after triaging when all criteria pass. label Jul 8, 2026
@Samin061

Copy link
Copy Markdown
Contributor Author

any update?

@potiuk potiuk left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — proper fix, and it's fixed in both places, which is the part that's easy to get wrong here.

The bug is real: f'attachment; filename="{basename}"' interpolates straight into a quoted header value, so a filename containing " closes the quote early and whatever follows is parsed as additional Content-Disposition parameters. Since filenames are frequently data-derived rather than literals in the Dag, that's reachable in practice — and on POSIX a filename can even contain a newline, which turns the same hole into MIME header injection rather than just a stray parameter.

part.add_header("Content-Disposition", "attachment", filename=basename) is the correct answer — it hands quoting and RFC 2231 encoding to the email library instead of hand-rolling it.

Two things I particularly liked:

Both copies are fixed. airflow-core/utils/email.py and providers/smtp/hooks/smtp.py carry duplicate implementations of this logic; patching one and leaving the other is exactly how a fix like this ends up half-applied. (Worth someone eventually collapsing that duplication — not this PR's job.)

The tests prove non-injection rather than asserting on a string. Using report.txt"; x-evil="1 as a real payload and then checking both that get_filename() round-trips intact and that x-evil never appears in get_params(header="Content-Disposition") demonstrates the escape actually holds. Mirroring it in both test files matches the dual fix.


Drafted-by: Claude Code (Opus 5); reviewed by @potiuk before posting

@potiuk
potiuk merged commit 5cc898f into apache:main Jul 31, 2026
146 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:providers provider:smtp ready for maintainer review Set after triaging when all criteria pass.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants