Do not allow smtp to access insecure files (#104972)

This commit is contained in:
Jan Bouwhuis 2023-12-04 02:06:01 +01:00 committed by GitHub
parent c1f68c3767
commit fe2906f159
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 53 additions and 9 deletions

View file

@ -8,6 +8,7 @@ from email.mime.text import MIMEText
import email.utils import email.utils
import logging import logging
import os import os
from pathlib import Path
import smtplib import smtplib
import voluptuous as vol import voluptuous as vol
@ -193,10 +194,15 @@ class MailNotificationService(BaseNotificationService):
if data := kwargs.get(ATTR_DATA): if data := kwargs.get(ATTR_DATA):
if ATTR_HTML in data: if ATTR_HTML in data:
msg = _build_html_msg( msg = _build_html_msg(
message, data[ATTR_HTML], images=data.get(ATTR_IMAGES, []) self.hass,
message,
data[ATTR_HTML],
images=data.get(ATTR_IMAGES, []),
) )
else: else:
msg = _build_multipart_msg(message, images=data.get(ATTR_IMAGES, [])) msg = _build_multipart_msg(
self.hass, message, images=data.get(ATTR_IMAGES, [])
)
else: else:
msg = _build_text_msg(message) msg = _build_text_msg(message)
@ -241,13 +247,21 @@ def _build_text_msg(message):
return MIMEText(message) return MIMEText(message)
def _attach_file(atch_name, content_id=""): def _attach_file(hass, atch_name, content_id=""):
"""Create a message attachment. """Create a message attachment.
If MIMEImage is successful and content_id is passed (HTML), add images in-line. If MIMEImage is successful and content_id is passed (HTML), add images in-line.
Otherwise add them as attachments. Otherwise add them as attachments.
""" """
try: try:
file_path = Path(atch_name).parent
if not hass.config.is_allowed_path(str(file_path)):
_LOGGER.warning(
"'%s' is not secure to load data from, ignoring attachment '%s'!",
file_path,
atch_name,
)
return
with open(atch_name, "rb") as attachment_file: with open(atch_name, "rb") as attachment_file:
file_bytes = attachment_file.read() file_bytes = attachment_file.read()
except FileNotFoundError: except FileNotFoundError:
@ -277,22 +291,22 @@ def _attach_file(atch_name, content_id=""):
return attachment return attachment
def _build_multipart_msg(message, images): def _build_multipart_msg(hass, message, images):
"""Build Multipart message with images as attachments.""" """Build Multipart message with images as attachments."""
_LOGGER.debug("Building multipart email with image attachment(s)") _LOGGER.debug("Building multipart email with image attachme_build_html_msgnt(s)")
msg = MIMEMultipart() msg = MIMEMultipart()
body_txt = MIMEText(message) body_txt = MIMEText(message)
msg.attach(body_txt) msg.attach(body_txt)
for atch_name in images: for atch_name in images:
attachment = _attach_file(atch_name) attachment = _attach_file(hass, atch_name)
if attachment: if attachment:
msg.attach(attachment) msg.attach(attachment)
return msg return msg
def _build_html_msg(text, html, images): def _build_html_msg(hass, text, html, images):
"""Build Multipart message with in-line images and rich HTML (UTF-8).""" """Build Multipart message with in-line images and rich HTML (UTF-8)."""
_LOGGER.debug("Building HTML rich email") _LOGGER.debug("Building HTML rich email")
msg = MIMEMultipart("related") msg = MIMEMultipart("related")
@ -303,7 +317,7 @@ def _build_html_msg(text, html, images):
for atch_name in images: for atch_name in images:
name = os.path.basename(atch_name) name = os.path.basename(atch_name)
attachment = _attach_file(atch_name, name) attachment = _attach_file(hass, atch_name, name)
if attachment: if attachment:
msg.attach(attachment) msg.attach(attachment)
return msg return msg

View file

@ -1,4 +1,5 @@
"""The tests for the notify smtp platform.""" """The tests for the notify smtp platform."""
from pathlib import Path
import re import re
from unittest.mock import patch from unittest.mock import patch
@ -132,15 +133,44 @@ EMAIL_DATA = [
], ],
) )
def test_send_message( def test_send_message(
message_data, data, content_type, hass: HomeAssistant, message hass: HomeAssistant, message_data, data, content_type, message
) -> None: ) -> None:
"""Verify if we can send messages of all types correctly.""" """Verify if we can send messages of all types correctly."""
sample_email = "<mock@mock>" sample_email = "<mock@mock>"
message.hass = hass
hass.config.allowlist_external_dirs.add(Path("tests/testing_config").resolve())
with patch("email.utils.make_msgid", return_value=sample_email): with patch("email.utils.make_msgid", return_value=sample_email):
result, _ = message.send_message(message_data, data=data) result, _ = message.send_message(message_data, data=data)
assert content_type in result assert content_type in result
@pytest.mark.parametrize(
("message_data", "data", "content_type"),
[
(
"Test msg",
{"images": ["tests/testing_config/notify/test.jpg"]},
"Content-Type: multipart/mixed",
),
],
)
def test_sending_insecure_files_fails(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
message_data,
data,
content_type,
message,
) -> None:
"""Verify if we cannot send messages with insecure attachments."""
sample_email = "<mock@mock>"
message.hass = hass
with patch("email.utils.make_msgid", return_value=sample_email):
result, _ = message.send_message(message_data, data=data)
assert content_type in result
assert "test.jpg' is not secure to load data from, ignoring attachment"
def test_send_text_message(hass: HomeAssistant, message) -> None: def test_send_text_message(hass: HomeAssistant, message) -> None:
"""Verify if we can send simple text message.""" """Verify if we can send simple text message."""
expected = ( expected = (