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

Reply via email to