1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136
|
From 937ac1fe069a4dc8471dff205f553d82e724015b Mon Sep 17 00:00:00 2001
From: Ned Deily <nad@python.org>
Date: Sun, 11 Mar 2018 14:29:05 -0400
Subject: [PATCH] [3.5] bpo-32981: Fix catastrophic backtracking vulns
(GH-5955) (#6034)
* Prevent low-grade poplib REDOS (CVE-2018-1060)
The regex to test a mail server's timestamp is susceptible to
catastrophic backtracking on long evil responses from the server.
Happily, the maximum length of malicious inputs is 2K thanks
to a limit introduced in the fix for CVE-2013-1752.
A 2KB evil response from the mail server would result in small slowdowns
(milliseconds vs. microseconds) accumulated over many apop calls.
This is a potential DOS vector via accumulated slowdowns.
Replace it with a similar non-vulnerable regex.
The new regex is RFC compliant.
The old regex was non-compliant in edge cases.
* Prevent difflib REDOS (CVE-2018-1061)
The default regex for IS_LINE_JUNK is susceptible to
catastrophic backtracking.
This is a potential DOS vector.
Replace it with an equivalent non-vulnerable regex.
Also introduce unit and REDOS tests for difflib.
Co-authored-by: Tim Peters <tim.peters@gmail.com>
Co-authored-by: Christian Heimes <christian@python.org>.
(cherry picked from commit 0e6c8ee2358a2e23117501826c008842acb835ac)
---
Lib/difflib.py | 2 +-
Lib/poplib.py | 2 +-
Lib/test/test_difflib.py | 22 ++++++++++++++++++-
Lib/test/test_poplib.py | 12 +++++++++-
Misc/ACKS | 1 +
.../2018-03-02-10-24-52.bpo-32981.O_qDyj.rst | 4 ++++
6 files changed, 39 insertions(+), 4 deletions(-)
create mode 100644 Misc/NEWS.d/next/Security/2018-03-02-10-24-52.bpo-32981.O_qDyj.rst
diff --git a/Lib/difflib.py b/Lib/difflib.py
index 076bbac01dee..b4ec33505644 100644
--- a/Lib/difflib.py
+++ b/Lib/difflib.py
@@ -1083,7 +1083,7 @@ def _qformat(self, aline, bline, atags, btags):
import re
-def IS_LINE_JUNK(line, pat=re.compile(r"\s*#?\s*$").match):
+def IS_LINE_JUNK(line, pat=re.compile(r"\s*(?:#\s*)?$").match):
r"""
Return 1 for ignorable line: iff `line` is blank or contains a single '#'.
diff --git a/Lib/poplib.py b/Lib/poplib.py
index 516b6f060d28..2437ea0e2717 100644
--- a/Lib/poplib.py
+++ b/Lib/poplib.py
@@ -308,7 +308,7 @@ def rpop(self, user):
return self._shortcmd('RPOP %s' % user)
- timestamp = re.compile(br'\+OK.*(<[^>]+>)')
+ timestamp = re.compile(br'\+OK.[^<]*(<.*>)')
def apop(self, user, password):
"""Authorisation
diff --git a/Lib/test/test_difflib.py b/Lib/test/test_difflib.py
index ab9debf8e252..b6c8a7dd5bed 100644
--- a/Lib/test/test_difflib.py
+++ b/Lib/test/test_difflib.py
@@ -466,13 +466,33 @@ def _assert_type_error(self, msg, generator, *args):
list(generator(*args))
self.assertEqual(msg, str(ctx.exception))
+class TestJunkAPIs(unittest.TestCase):
+ def test_is_line_junk_true(self):
+ for line in ['#', ' ', ' #', '# ', ' # ', '']:
+ self.assertTrue(difflib.IS_LINE_JUNK(line), repr(line))
+
+ def test_is_line_junk_false(self):
+ for line in ['##', ' ##', '## ', 'abc ', 'abc #', 'Mr. Moose is up!']:
+ self.assertFalse(difflib.IS_LINE_JUNK(line), repr(line))
+
+ def test_is_line_junk_REDOS(self):
+ evil_input = ('\t' * 1000000) + '##'
+ self.assertFalse(difflib.IS_LINE_JUNK(evil_input))
+
+ def test_is_character_junk_true(self):
+ for char in [' ', '\t']:
+ self.assertTrue(difflib.IS_CHARACTER_JUNK(char), repr(char))
+
+ def test_is_character_junk_false(self):
+ for char in ['a', '#', '\n', '\f', '\r', '\v']:
+ self.assertFalse(difflib.IS_CHARACTER_JUNK(char), repr(char))
def test_main():
difflib.HtmlDiff._default_prefix = 0
Doctests = doctest.DocTestSuite(difflib)
run_unittest(
TestWithAscii, TestAutojunk, TestSFpatches, TestSFbugs,
- TestOutputFormat, TestBytes, Doctests)
+ TestOutputFormat, TestBytes, TestJunkAPIs, Doctests)
if __name__ == '__main__':
test_main()
diff --git a/Lib/test/test_poplib.py b/Lib/test/test_poplib.py
index bceeb93ad14a..799e40365214 100644
--- a/Lib/test/test_poplib.py
+++ b/Lib/test/test_poplib.py
@@ -300,9 +300,19 @@ def test_noop(self):
def test_rpop(self):
self.assertOK(self.client.rpop('foo'))
- def test_apop(self):
+ def test_apop_normal(self):
self.assertOK(self.client.apop('foo', 'dummypassword'))
+ def test_apop_REDOS(self):
+ # Replace welcome with very long evil welcome.
+ # NB The upper bound on welcome length is currently 2048.
+ # At this length, evil input makes each apop call take
+ # on the order of milliseconds instead of microseconds.
+ evil_welcome = b'+OK' + (b'<' * 1000000)
+ with test_support.swap_attr(self.client, 'welcome', evil_welcome):
+ # The evil welcome is invalid, so apop should throw.
+ self.assertRaises(poplib.error_proto, self.client.apop, 'a', 'kb')
+
def test_top(self):
expected = (b'+OK 116 bytes',
[b'From: postmaster@python.org', b'Content-Type: text/plain',
|