diff --git a/email_profile/parser.py b/email_profile/parser.py index b86fdad..cc43bdb 100644 --- a/email_profile/parser.py +++ b/email_profile/parser.py @@ -213,11 +213,15 @@ def parse_rfc822(raw_message: bytes) -> ParsedBody: if not filename: continue + safe_name = Path(_decode_header(filename).replace("\\", "/")).name + if not safe_name or safe_name in (".", ".."): + continue + payload = part.get_payload(decode=True) or b"" body.attachments.append( Attachment( - file_name=_decode_header(filename), + file_name=safe_name, content_type=content_type, content=( payload diff --git a/tests/test_parser.py b/tests/test_parser.py index fbc6163..bc175d0 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -42,6 +42,32 @@ def test_content(self): ) +class TestAttachmentPathTraversal(TestCase): + def _msg(self, filename: str) -> bytes: + return ( + b"From: a@x\r\nTo: b@x\r\nSubject: t\r\n" + b'Content-Type: multipart/mixed; boundary="B"\r\n\r\n' + b"--B\r\nContent-Type: text/plain\r\n\r\nbody\r\n" + b"--B\r\nContent-Disposition: attachment; " + b'filename="' + filename.encode() + b'"\r\n\r\n' + b"payload\r\n--B--\r\n" + ) + + def test_unix_traversal_stripped(self): + atts = parse_rfc822(self._msg("../../etc/passwd")).attachments + self.assertEqual(len(atts), 1) + self.assertEqual(atts[0].file_name, "passwd") + + def test_windows_traversal_stripped(self): + atts = parse_rfc822( + self._msg("..\\\\..\\\\windows\\\\evil.exe") + ).attachments + self.assertEqual(atts[0].file_name, "evil.exe") + + def test_pure_traversal_dropped(self): + self.assertEqual(parse_rfc822(self._msg("../..")).attachments, []) + + class TestNamedHeaders(TestCase): def test_named_headers_extracted(self): raw = (