This is an automated email from the ASF dual-hosted git repository.
potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new 5cc898f8743 Escape attachment filename in Content-Disposition header
(#69435)
5cc898f8743 is described below
commit 5cc898f8743bba6174ab9b7b627523a7f333fdd7
Author: Samina <[email protected]>
AuthorDate: Sat Aug 1 00:39:43 2026 +0530
Escape attachment filename in Content-Disposition header (#69435)
---
airflow-core/src/airflow/utils/email.py | 2 +-
airflow-core/tests/unit/utils/test_email.py | 19 +++++++++++++++++++
.../smtp/src/airflow/providers/smtp/hooks/smtp.py | 2 +-
providers/smtp/tests/unit/smtp/hooks/test_smtp.py | 20 ++++++++++++++++++++
4 files changed, 41 insertions(+), 2 deletions(-)
diff --git a/airflow-core/src/airflow/utils/email.py
b/airflow-core/src/airflow/utils/email.py
index 393841efa3d..46f24fba0f5 100644
--- a/airflow-core/src/airflow/utils/email.py
+++ b/airflow-core/src/airflow/utils/email.py
@@ -208,7 +208,7 @@ def build_mime_message(
basename = os.path.basename(fname)
with open(fname, "rb") as file:
part = MIMEApplication(file.read(), Name=basename)
- part["Content-Disposition"] = f'attachment; filename="{basename}"'
+ part.add_header("Content-Disposition", "attachment",
filename=basename)
part["Content-ID"] = f"<{basename}>"
msg.attach(part)
diff --git a/airflow-core/tests/unit/utils/test_email.py
b/airflow-core/tests/unit/utils/test_email.py
index f73edeb3679..33f242fc72f 100644
--- a/airflow-core/tests/unit/utils/test_email.py
+++ b/airflow-core/tests/unit/utils/test_email.py
@@ -146,6 +146,25 @@ class TestEmail:
assert [mail_to] == recipients
assert msg["To"] == ",".join(recipients)
+ def test_build_mime_message_escapes_attachment_filename(self, tmp_path):
+ # A quote in the filename must not break out of the quoted
+ # Content-Disposition value and inject extra parameters.
+ malicious = 'report.txt"; x-evil="1'
+ attachment = tmp_path / malicious
+ attachment.write_bytes(b"data")
+
+ msg, _ = email.build_mime_message(
+ mail_from="[email protected]",
+ to="[email protected]",
+ subject="subject",
+ html_content="<html></html>",
+ files=[os.fspath(attachment)],
+ )
+
+ part = msg.get_payload()[-1]
+ assert part.get_filename() == malicious
+ assert "x-evil" not in
dict(part.get_params(header="Content-Disposition"))
+
@pytest.mark.db_test
class TestEmailSmtp:
diff --git a/providers/smtp/src/airflow/providers/smtp/hooks/smtp.py
b/providers/smtp/src/airflow/providers/smtp/hooks/smtp.py
index 991d13c1abc..d6b56c11988 100644
--- a/providers/smtp/src/airflow/providers/smtp/hooks/smtp.py
+++ b/providers/smtp/src/airflow/providers/smtp/hooks/smtp.py
@@ -573,7 +573,7 @@ class SmtpHook(BaseHook):
basename = os.path.basename(fname)
with open(fname, "rb") as file:
part = MIMEApplication(file.read(), Name=basename)
- part["Content-Disposition"] = f'attachment;
filename="{basename}"'
+ part.add_header("Content-Disposition", "attachment",
filename=basename)
part["Content-ID"] = f"<{basename}>"
msg.attach(part)
diff --git a/providers/smtp/tests/unit/smtp/hooks/test_smtp.py
b/providers/smtp/tests/unit/smtp/hooks/test_smtp.py
index 13e494306c7..e3a3f557f70 100644
--- a/providers/smtp/tests/unit/smtp/hooks/test_smtp.py
+++ b/providers/smtp/tests/unit/smtp/hooks/test_smtp.py
@@ -235,6 +235,26 @@ class TestSmtpHook:
assert [mail_to] == recipients
assert msg["To"] == ",".join(recipients)
+ @patch(smtplib_string)
+ def test_build_mime_message_escapes_attachment_filename(self,
mock_smtplib, tmp_path):
+ # A quote in the filename must not break out of the quoted
+ # Content-Disposition value and inject extra parameters.
+ malicious = 'report.txt"; x-evil="1'
+ attachment = tmp_path / malicious
+ attachment.write_bytes(b"data")
+ with SmtpHook() as smtp_hook:
+ msg, _ = smtp_hook._build_mime_message(
+ mail_from=FROM_EMAIL,
+ to=TO_EMAIL,
+ subject=TEST_SUBJECT,
+ html_content=TEST_BODY,
+ files=[os.fspath(attachment)],
+ )
+
+ part = msg.get_payload()[-1]
+ assert part.get_filename() == malicious
+ assert "x-evil" not in
dict(part.get_params(header="Content-Disposition"))
+
@patch(smtplib_string)
def test_send_smtp(self, mock_smtplib):
mock_send_mime = mock_smtplib.SMTP_SSL().sendmail