This is an automated email from the ASF dual-hosted git repository.

sbp pushed a commit to branch sbp
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git


The following commit(s) were added to refs/heads/sbp by this push:
     new 7c8efdd4 Ensure that emails cannot contain null bytes
7c8efdd4 is described below

commit 7c8efdd4b39c069dde08f690fd147a20db06c05a
Author: Sean B. Palmer <[email protected]>
AuthorDate: Wed Mar 11 20:03:00 2026 +0000

    Ensure that emails cannot contain null bytes
---
 atr/mail.py             |  17 +++++++
 tests/unit/test_mail.py | 116 +++++++++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 132 insertions(+), 1 deletion(-)

diff --git a/atr/mail.py b/atr/mail.py
index 492cbc6e..7811f74c 100644
--- a/atr/mail.py
+++ b/atr/mail.py
@@ -53,6 +53,13 @@ class Message:
 async def send(msg_data: Message) -> tuple[str, list[str]]:
     """Send an email notification about an artifact or a vote."""
     log.info(f"Sending email for event: {msg_data}")
+    _reject_null_bytes(
+        msg_data.email_sender,
+        msg_data.email_recipient,
+        msg_data.subject,
+        msg_data.body,
+        msg_data.in_reply_to,
+    )
     from_addr = msg_data.email_sender
     if not from_addr.endswith(f"@{global_domain}"):
         raise ValueError(f"from_addr must end with @{global_domain}, got 
{from_addr}")
@@ -105,6 +112,12 @@ async def send(msg_data: Message) -> tuple[str, list[str]]:
     return mid, errors
 
 
+def _reject_null_bytes(*values: str | None) -> None:
+    for value in values:
+        if (value is not None) and ("\x00" in value):
+            raise ValueError("Email content cannot contain null bytes")
+
+
 async def _send_many(from_addr: str, to_addrs: list[str], msg_text: str) -> 
list[str]:
     """Send an email to multiple recipients."""
     message_bytes = bytes(msg_text, "utf-8")
@@ -147,6 +160,10 @@ async def _send_via_relay(from_addr: str, to_addr: str, 
msg_bytes: bytes) -> Non
 
 def _split_address(addr: str) -> tuple[str, str]:
     """Split an email address into local and domain parts."""
+    if "\x00" in addr:
+        raise ValueError("Email address cannot contain null bytes")
+    if ("\r" in addr) or ("\n" in addr):
+        raise ValueError("Email address cannot contain CR/LF characters")
     parts = addr.split("@", 1)
     if len(parts) != 2:
         raise ValueError("Invalid mail address")
diff --git a/tests/unit/test_mail.py b/tests/unit/test_mail.py
index e8586559..14b2fa76 100644
--- a/tests/unit/test_mail.py
+++ b/tests/unit/test_mail.py
@@ -321,7 +321,7 @@ async def test_send_rejects_crlf_in_to_address(monkeypatch: 
"MonkeyPatch") -> No
     )
 
     # Call send and expect it to raise ValueError due to invalid recipient 
format
-    with pytest.raises(ValueError, match=r"Email recipient must be 
@apache.org"):
+    with pytest.raises(ValueError, match=r"CR/LF"):
         await mail.send(malicious_message)
 
     # Assert that _send_many was never called
@@ -353,6 +353,102 @@ async def 
test_send_rejects_lf_only_injection(monkeypatch: "MonkeyPatch") -> Non
     mock_send_many.assert_not_called()
 
 
[email protected]
+async def test_send_rejects_null_byte_in_body(monkeypatch: "MonkeyPatch") -> 
None:
+    """Test that null bytes in body field are rejected."""
+    mock_send_many = mock.AsyncMock(return_value=[])
+    monkeypatch.setattr("atr.mail._send_many", mock_send_many)
+
+    malicious_message = mail.Message(
+        email_sender="[email protected]",
+        email_recipient="[email protected]",
+        subject="Test Subject",
+        body="Normal start\x00injected content",
+    )
+
+    with pytest.raises(ValueError, match=r"null bytes"):
+        await mail.send(malicious_message)
+
+    mock_send_many.assert_not_called()
+
+
[email protected]
+async def test_send_rejects_null_byte_in_from_address(monkeypatch: 
"MonkeyPatch") -> None:
+    """Test that null bytes in from address field are rejected."""
+    mock_send_many = mock.AsyncMock(return_value=[])
+    monkeypatch.setattr("atr.mail._send_many", mock_send_many)
+
+    malicious_message = mail.Message(
+        email_sender="sender\[email protected]",
+        email_recipient="[email protected]",
+        subject="Test Subject",
+        body="This is a test message",
+    )
+
+    with pytest.raises(ValueError, match=r"null bytes"):
+        await mail.send(malicious_message)
+
+    mock_send_many.assert_not_called()
+
+
[email protected]
+async def test_send_rejects_null_byte_in_reply_to(monkeypatch: "MonkeyPatch") 
-> None:
+    """Test that null bytes in in_reply_to field are rejected."""
+    mock_send_many = mock.AsyncMock(return_value=[])
+    monkeypatch.setattr("atr.mail._send_many", mock_send_many)
+
+    malicious_message = mail.Message(
+        email_sender="[email protected]",
+        email_recipient="[email protected]",
+        subject="Test Subject",
+        body="This is a test message",
+        in_reply_to="valid-id\[email protected]",
+    )
+
+    with pytest.raises(ValueError, match=r"null bytes"):
+        await mail.send(malicious_message)
+
+    mock_send_many.assert_not_called()
+
+
[email protected]
+async def test_send_rejects_null_byte_in_subject(monkeypatch: "MonkeyPatch") 
-> None:
+    """Test that null bytes in subject field are rejected."""
+    mock_send_many = mock.AsyncMock(return_value=[])
+    monkeypatch.setattr("atr.mail._send_many", mock_send_many)
+
+    malicious_message = mail.Message(
+        email_sender="[email protected]",
+        email_recipient="[email protected]",
+        subject="Legitimate Subject\x00Bcc: [email protected]",
+        body="This is a test message",
+    )
+
+    with pytest.raises(ValueError, match=r"null bytes"):
+        await mail.send(malicious_message)
+
+    mock_send_many.assert_not_called()
+
+
[email protected]
+async def test_send_rejects_null_byte_in_to_address(monkeypatch: 
"MonkeyPatch") -> None:
+    """Test that null bytes in to address field are rejected."""
+    mock_send_many = mock.AsyncMock(return_value=[])
+    monkeypatch.setattr("atr.mail._send_many", mock_send_many)
+
+    malicious_message = mail.Message(
+        email_sender="[email protected]",
+        email_recipient="recipient\[email protected]",
+        subject="Test Subject",
+        body="This is a test message",
+    )
+
+    with pytest.raises(ValueError, match=r"null bytes"):
+        await mail.send(malicious_message)
+
+    mock_send_many.assert_not_called()
+
+
 def test_smtp_policy_vs_smtputf8() -> None:
     """Test that SMTPUTF8 policy is required for proper Unicode handling.
 
@@ -383,3 +479,21 @@ def test_smtp_policy_vs_smtputf8() -> None:
     # SMTPUTF8 preserves the character directly
     assert "Test avec é" in smtputf8_str
     assert "=?utf-8?" not in smtputf8_str
+
+
+def test_split_address_rejects_cr() -> None:
+    """Test that _split_address rejects addresses containing CR."""
+    with pytest.raises(ValueError, match=r"CR/LF"):
+        mail._split_address("user\[email protected]")
+
+
+def test_split_address_rejects_lf() -> None:
+    """Test that _split_address rejects addresses containing LF."""
+    with pytest.raises(ValueError, match=r"CR/LF"):
+        mail._split_address("user\[email protected]")
+
+
+def test_split_address_rejects_null_byte() -> None:
+    """Test that _split_address rejects addresses containing null bytes."""
+    with pytest.raises(ValueError, match=r"null bytes"):
+        mail._split_address("user\[email protected]")


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to