Skip to content

Commit 11afbbe

Browse files
miss-islingtonmgorny
authored andcommitted
bpo-43124: Fix smtplib multiple CRLF injection (pythonGH-25987) (pythonGH-28038)
Co-authored-by: Miguel Brito <5544985+miguendes@users.noreply.github.com> Co-authored-by: Łukasz Langa <lukasz@langa.pl> (cherry picked from commit 0897253) (updated for 2.7 by Michał Górny)
1 parent dae96ac commit 11afbbe

3 files changed

Lines changed: 76 additions & 3 deletions

File tree

Lib/smtplib.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -336,10 +336,16 @@ def send(self, str):
336336
def putcmd(self, cmd, args=""):
337337
"""Send a command to the server."""
338338
if args == "":
339-
str = '%s%s' % (cmd, CRLF)
339+
s = cmd
340340
else:
341-
str = '%s %s%s' % (cmd, args, CRLF)
342-
self.send(str)
341+
s = '%s %s' % (cmd, args)
342+
if '\r' in s or '\n' in s:
343+
s = s.replace('\n', '\\n').replace('\r', '\\r')
344+
raise ValueError(
345+
'command and arguments contain prohibited newline characters: %s'
346+
% (s,)
347+
)
348+
self.send(s + CRLF)
343349

344350
def getreply(self):
345351
"""Get a reply from the server.

Lib/test/test_smtplib.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,9 @@ def setUp(self):
155155
self._threads = test_support.threading_setup()
156156
self.serv_evt = threading.Event()
157157
self.client_evt = threading.Event()
158+
# Capture SMTPChannel debug output
159+
self.old_DEBUGSTREAM = smtpd.DEBUGSTREAM
160+
smtpd.DEBUGSTREAM = StringIO.StringIO()
158161
# Pick a random unused port by passing 0 for the port number
159162
self.serv = smtpd.DebuggingServer((HOST, 0), ('nowhere', -1))
160163
# Keep a note of what port was assigned
@@ -176,6 +179,9 @@ def tearDown(self):
176179
test_support.threading_cleanup(*self._threads)
177180
# restore sys.stdout
178181
sys.stdout = self.old_stdout
182+
# restore DEBUGSTREAM
183+
smtpd.DEBUGSTREAM.close()
184+
smtpd.DEBUGSTREAM = self.old_DEBUGSTREAM
179185

180186
def testBasic(self):
181187
# connect
@@ -201,6 +207,17 @@ def testNotImplemented(self):
201207
self.assertEqual(smtp.ehlo(), expected)
202208
smtp.quit()
203209

210+
211+
def test_issue43124_putcmd_escapes_newline(self):
212+
# see: https://bugs.python.org/issue43124
213+
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost',
214+
timeout=10) # support.LOOPBACK_TIMEOUT in newer Pythons
215+
self.addCleanup(smtp.close)
216+
with self.assertRaises(ValueError) as exc:
217+
smtp.putcmd('helo\nX-INJECTED')
218+
self.assertIn("prohibited newline characters", str(exc.exception))
219+
smtp.quit()
220+
204221
def testVRFY(self):
205222
# VRFY isn't implemented in DebuggingServer
206223
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=15)
@@ -240,6 +257,54 @@ def testSend(self):
240257
mexpect = '%s%s\n%s' % (MSG_BEGIN, m, MSG_END)
241258
self.assertEqual(self.output.getvalue(), mexpect)
242259

260+
def test_issue43124_escape_localhostname(self):
261+
# see: https://bugs.python.org/issue43124
262+
# connect and send mail
263+
m = 'wazzuuup\nlinetwo'
264+
smtp = smtplib.SMTP(HOST, self.port, local_hostname='hi\nX-INJECTED',
265+
timeout=10) # support.LOOPBACK_TIMEOUT in newer Pythons
266+
self.addCleanup(smtp.close)
267+
with self.assertRaises(ValueError) as exc:
268+
smtp.sendmail("hi@me.com", "you@me.com", m)
269+
self.assertIn(
270+
"prohibited newline characters: ehlo hi\\nX-INJECTED",
271+
str(exc.exception),
272+
)
273+
# XXX (see comment in testSend)
274+
time.sleep(0.01)
275+
smtp.quit()
276+
277+
debugout = smtpd.DEBUGSTREAM.getvalue()
278+
self.assertNotIn("X-INJECTED", debugout)
279+
280+
def test_issue43124_escape_options(self):
281+
# see: https://bugs.python.org/issue43124
282+
# connect and send mail
283+
m = 'wazzuuup\nlinetwo'
284+
smtp = smtplib.SMTP(
285+
HOST, self.port, local_hostname='localhost',
286+
timeout=10) # support.LOOPBACK_TIMEOUT in newer Pythons
287+
288+
self.addCleanup(smtp.close)
289+
# smtp.sendmail("hi@me.com", "you@me.com", m)
290+
# NB: gross hack but still cleaner than backporting whole ESMTP
291+
# support to DebuggingServer
292+
smtp.does_esmtp = 1
293+
with self.assertRaises(ValueError) as exc:
294+
smtp.mail("hi@me.com", ["X-OPTION\nX-INJECTED-1", "X-OPTION2\nX-INJECTED-2"])
295+
msg = str(exc.exception)
296+
self.assertIn("prohibited newline characters", msg)
297+
self.assertIn("X-OPTION\\nX-INJECTED-1 X-OPTION2\\nX-INJECTED-2", msg)
298+
# XXX (see comment in testSend)
299+
time.sleep(0.01)
300+
smtp.quit()
301+
302+
debugout = smtpd.DEBUGSTREAM.getvalue()
303+
self.assertNotIn("X-OPTION", debugout)
304+
self.assertNotIn("X-OPTION2", debugout)
305+
self.assertNotIn("X-INJECTED-1", debugout)
306+
self.assertNotIn("X-INJECTED-2", debugout)
307+
243308

244309
class NonConnectingTests(unittest.TestCase):
245310

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Made the internal ``putcmd`` function in :mod:`smtplib` sanitize input for
2+
presence of ``\r`` and ``\n`` characters to avoid (unlikely) command injection.

0 commit comments

Comments
 (0)