From 22d50e7757bb11e1986f9ba670a9bab01afcb9d6 Mon Sep 17 00:00:00 2001 From: Mat Booth Date: Apr 13 2015 09:48:58 +0000 Subject: Fix CVE-2013-1752 - multiple unbound readline() DoS flaws in python stdlib --- diff --git a/jython-CVE-2013-1752.patch b/jython-CVE-2013-1752.patch new file mode 100644 index 0000000..996973b --- /dev/null +++ b/jython-CVE-2013-1752.patch @@ -0,0 +1,471 @@ +diff --git a/Lib/ftplib.py b/Lib/ftplib.py +--- a/Lib/ftplib.py ++++ b/Lib/ftplib.py +@@ -55,6 +55,8 @@ MSG_OOB = 0x1 + + # The standard FTP server control port + FTP_PORT = 21 ++# The sizehint parameter passed to readline() calls ++MAXLINE = 8192 + + + # Exception raised when an error or invalid response is received +@@ -101,6 +103,7 @@ class FTP: + debugging = 0 + host = '' + port = FTP_PORT ++ maxline = MAXLINE + sock = None + file = None + welcome = None +@@ -180,7 +183,9 @@ class FTP: + # Internal: return one line from the server, stripping CRLF. + # Raise EOFError if the connection is closed + def getline(self): +- line = self.file.readline() ++ line = self.file.readline(self.maxline + 1) ++ if len(line) > self.maxline: ++ raise Error("got more than %d bytes" % self.maxline) + if self.debugging > 1: + print '*get*', self.sanitize(line) + if not line: raise EOFError +@@ -432,7 +437,9 @@ class FTP: + conn = self.transfercmd(cmd) + fp = conn.makefile('rb') + while 1: +- line = fp.readline() ++ line = fp.readline(self.maxline + 1) ++ if len(line) > self.maxline: ++ raise Error("got more than %d bytes" % self.maxline) + if self.debugging > 2: print '*retr*', repr(line) + if not line: + break +@@ -485,7 +492,9 @@ class FTP: + self.voidcmd('TYPE A') + conn = self.transfercmd(cmd) + while 1: +- buf = fp.readline() ++ buf = fp.readline(self.maxline + 1) ++ if len(buf) > self.maxline: ++ raise Error("got more than %d bytes" % self.maxline) + if not buf: break + if buf[-2:] != CRLF: + if buf[-1] in CRLF: buf = buf[:-1] +@@ -710,7 +719,9 @@ else: + fp = conn.makefile('rb') + try: + while 1: +- line = fp.readline() ++ line = fp.readline(self.maxline + 1) ++ if len(line) > self.maxline: ++ raise Error("got more than %d bytes" % self.maxline) + if self.debugging > 2: print '*retr*', repr(line) + if not line: + break +@@ -748,7 +759,9 @@ else: + conn = self.transfercmd(cmd) + try: + while 1: +- buf = fp.readline() ++ buf = fp.readline(self.maxline + 1) ++ if len(buf) > self.maxline: ++ raise Error("got more than %d bytes" % self.maxline) + if not buf: break + if buf[-2:] != CRLF: + if buf[-1] in CRLF: buf = buf[:-1] +@@ -905,7 +918,9 @@ class Netrc: + fp = open(filename, "r") + in_macro = 0 + while 1: +- line = fp.readline() ++ line = fp.readline(self.maxline + 1) ++ if len(line) > self.maxline: ++ raise Error("got more than %d bytes" % self.maxline) + if not line: break + if in_macro and line.strip(): + macro_lines.append(line) +diff --git a/Lib/test/test_ftplib.py b/Lib/test/test_ftplib.py +--- a/Lib/test/test_ftplib.py ++++ b/Lib/test/test_ftplib.py +@@ -65,6 +65,7 @@ class DummyFTPHandler(asynchat.async_cha + self.last_received_data = '' + self.next_response = '' + self.rest = None ++ self.next_retr_data = RETR_DATA + self.push('220 welcome') + + def collect_incoming_data(self, data): +@@ -189,7 +190,7 @@ class DummyFTPHandler(asynchat.async_cha + offset = int(self.rest) + else: + offset = 0 +- self.dtp.push(RETR_DATA[offset:]) ++ self.dtp.push(self.next_retr_data[offset:]) + self.dtp.close_when_done() + self.rest = None + +@@ -203,6 +204,11 @@ class DummyFTPHandler(asynchat.async_cha + self.dtp.push(NLST_DATA) + self.dtp.close_when_done() + ++ def cmd_setlongretr(self, arg): ++ # For testing. Next RETR will return long line. ++ self.next_retr_data = 'x' * int(arg) ++ self.push('125 setlongretr ok') ++ + + class DummyFTPServer(asyncore.dispatcher, threading.Thread): + +@@ -558,6 +564,20 @@ class TestFTPClass(TestCase): + # IPv4 is in use, just make sure send_epsv has not been used + self.assertEqual(self.server.handler.last_received_cmd, 'pasv') + ++ def test_line_too_long(self): ++ self.assertRaises(ftplib.Error, self.client.sendcmd, ++ 'x' * self.client.maxline * 2) ++ ++ def test_retrlines_too_long(self): ++ self.client.sendcmd('SETLONGRETR %d' % (self.client.maxline * 2)) ++ received = [] ++ self.assertRaises(ftplib.Error, ++ self.client.retrlines, 'retr', received.append) ++ ++ def test_storlines_too_long(self): ++ f = StringIO.StringIO('x' * self.client.maxline * 2) ++ self.assertRaises(ftplib.Error, self.client.storlines, 'stor', f) ++ + + class TestIPv6Environment(TestCase): + +diff --git a/Lib/imaplib.py b/Lib/imaplib.py +--- a/Lib/imaplib.py ++++ b/Lib/imaplib.py +@@ -35,6 +35,15 @@ IMAP4_PORT = 143 + IMAP4_SSL_PORT = 993 + AllowedVersions = ('IMAP4REV1', 'IMAP4') # Most recent first + ++# Maximal line length when calling readline(). This is to prevent ++# reading arbitrary length lines. RFC 3501 and 2060 (IMAP 4rev1) ++# don't specify a line length. RFC 2683 however suggests limiting client ++# command lines to 1000 octets and server command lines to 8000 octets. ++# We have selected 10000 for some extra margin and since that is supposedly ++# also what UW and Panda IMAP does. ++_MAXLINE = 10000 ++ ++ + # Commands + + Commands = { +@@ -237,7 +246,10 @@ class IMAP4: + + def readline(self): + """Read line from remote.""" +- return self.file.readline() ++ line = self.file.readline(_MAXLINE + 1) ++ if len(line) > _MAXLINE: ++ raise self.error("got more than %d bytes" % _MAXLINE) ++ return line + + + def send(self, data): +diff --git a/Lib/test/test_imaplib.py b/Lib/test/test_imaplib.py +--- a/Lib/test/test_imaplib.py ++++ b/Lib/test/test_imaplib.py +@@ -165,6 +165,16 @@ class BaseThreadedNetworkedTests(unittes + self.imap_class, *server.server_address) + + ++ def test_linetoolong(self): ++ class TooLongHandler(SimpleIMAPHandler): ++ def handle(self): ++ # Send a very long response line ++ self.wfile.write('* OK ' + imaplib._MAXLINE*'x' + '\r\n') ++ ++ with self.reaped_server(TooLongHandler) as server: ++ self.assertRaises(imaplib.IMAP4.error, ++ self.imap_class, *server.server_address) ++ + class ThreadedNetworkedTests(BaseThreadedNetworkedTests): + + server_class = SocketServer.TCPServer + +diff --git a/Lib/nntplib.py b/Lib/nntplib.py +--- a/Lib/nntplib.py ++++ b/Lib/nntplib.py +@@ -37,6 +37,13 @@ import socket + "error_reply","error_temp","error_perm","error_proto", + "error_data",] + ++# maximal line length when calling readline(). This is to prevent ++# reading arbitrary lenght lines. RFC 3977 limits NNTP line length to ++# 512 characters, including CRLF. We have selected 2048 just to be on ++# the safe side. ++_MAXLINE = 2048 ++ ++ + # Exceptions raised when an error or invalid response is received + class NNTPError(Exception): + """Base class for all nntplib exceptions""" +@@ -200,7 +207,9 @@ class NNTP: + def getline(self): + """Internal: return one line from the server, stripping CRLF. + Raise EOFError if the connection is closed.""" +- line = self.file.readline() ++ line = self.file.readline(_MAXLINE + 1) ++ if len(line) > _MAXLINE: ++ raise NNTPDataError('line too long') + if self.debugging > 1: + print '*get*', repr(line) + if not line: raise EOFError +diff --git a/Lib/test/test_nntplib.py b/Lib/test/test_nntplib.py +new file mode 100644 +--- /dev/null ++++ b/Lib/test/test_nntplib.py +@@ -0,0 +1,65 @@ ++import socket ++import threading ++import nntplib ++import time ++ ++from unittest import TestCase ++from test import test_support ++ ++HOST = test_support.HOST ++ ++ ++def server(evt, serv, evil=False): ++ serv.listen(5) ++ try: ++ conn, addr = serv.accept() ++ except socket.timeout: ++ pass ++ else: ++ if evil: ++ conn.send("1 I'm too long response" * 3000 + "\n") ++ else: ++ conn.send("1 I'm OK response\n") ++ conn.close() ++ finally: ++ serv.close() ++ evt.set() ++ ++ ++class BaseServerTest(TestCase): ++ def setUp(self): ++ self.evt = threading.Event() ++ self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) ++ self.sock.settimeout(3) ++ self.port = test_support.bind_port(self.sock) ++ threading.Thread( ++ target=server, ++ args=(self.evt, self.sock, self.evil)).start() ++ time.sleep(.1) ++ ++ def tearDown(self): ++ self.evt.wait() ++ ++ ++class ServerTests(BaseServerTest): ++ evil = False ++ ++ def test_basic_connect(self): ++ nntp = nntplib.NNTP('localhost', self.port) ++ nntp.sock.close() ++ ++ ++class EvilServerTests(BaseServerTest): ++ evil = True ++ ++ def test_too_long_line(self): ++ self.assertRaises(nntplib.NNTPDataError, ++ nntplib.NNTP, 'localhost', self.port) ++ ++ ++def test_main(verbose=None): ++ test_support.run_unittest(EvilServerTests) ++ test_support.run_unittest(ServerTests) ++ ++if __name__ == '__main__': ++ test_main() +diff --git a/Lib/poplib.py b/Lib/poplib.py +--- a/Lib/poplib.py ++++ b/Lib/poplib.py +@@ -32,6 +32,12 @@ CR = '\r' + LF = '\n' + CRLF = CR+LF + ++# maximal line length when calling readline(). This is to prevent ++# reading arbitrary length lines. RFC 1939 limits POP3 line length to ++# 512 characters, including CRLF. We have selected 2048 just to be on ++# the safe side. ++_MAXLINE = 2048 ++ + + class POP3: + +@@ -103,7 +109,9 @@ class POP3: + # Raise error_proto('-ERR EOF') if the connection is closed. + + def _getline(self): +- line = self.file.readline() ++ line = self.file.readline(_MAXLINE + 1) ++ if len(line) > _MAXLINE: ++ raise error_proto('line too long') + if self._debugging > 1: print '*get*', repr(line) + if not line: raise error_proto('-ERR EOF') + octets = len(line) +@@ -365,6 +373,8 @@ else: + match = renewline.match(self.buffer) + while not match: + self._fillBuffer() ++ if len(self.buffer) > _MAXLINE: ++ raise error_proto('line too long') + match = renewline.match(self.buffer) + line = match.group(0) + self.buffer = renewline.sub('' ,self.buffer, 1) +diff --git a/Lib/test/test_poplib.py b/Lib/test/test_poplib.py +--- a/Lib/test/test_poplib.py ++++ b/Lib/test/test_poplib.py +@@ -198,6 +198,10 @@ class TestPOP3Class(TestCase): + 113) + self.assertEqual(self.client.retr('foo'), expected) + ++ def test_too_long_lines(self): ++ self.assertRaises(poplib.error_proto, self.client._shortcmd, ++ 'echo +%s' % ((poplib._MAXLINE + 10) * 'a')) ++ + def test_dele(self): + self.assertOK(self.client.dele('foo')) + +diff --git a/Lib/smtplib.py b/Lib/smtplib.py +--- a/Lib/smtplib.py ++++ b/Lib/smtplib.py +@@ -57,6 +57,7 @@ from sys import stderr + SMTP_PORT = 25 + SMTP_SSL_PORT = 465 + CRLF = "\r\n" ++_MAXLINE = 8192 # more than 8 times larger than RFC 821, 4.5.3 + + OLDSTYLE_AUTH = re.compile(r"auth=(.*)", re.I) + +@@ -179,10 +180,14 @@ else: + def __init__(self, sslobj): + self.sslobj = sslobj + +- def readline(self): ++ def readline(self, size=-1): ++ if size < 0: ++ size = None + str = "" + chr = None + while chr != "\n": ++ if size is not None and len(str) >= size: ++ break + chr = self.sslobj.read(1) + if not chr: + break +@@ -353,7 +358,7 @@ class SMTP: + self.file = self.sock.makefile('rb') + while 1: + try: +- line = self.file.readline() ++ line = self.file.readline(_MAXLINE + 1) + except socket.error as e: + self.close() + raise SMTPServerDisconnected("Connection unexpectedly closed: " +@@ -363,6 +368,8 @@ class SMTP: + raise SMTPServerDisconnected("Connection unexpectedly closed") + if self.debuglevel > 0: + print>>stderr, 'reply:', repr(line) ++ if len(line) > _MAXLINE: ++ raise SMTPResponseException(500, "Line too long.") + resp.append(line[4:].strip()) + code = line[:3] + # Check that the error code is syntactically correct. +diff --git a/Lib/test/test_smtplib.py b/Lib/test/test_smtplib.py +--- a/Lib/test/test_smtplib.py ++++ b/Lib/test/test_smtplib.py +@@ -292,6 +292,33 @@ class BadHELOServerTests(unittest.TestCa + HOST, self.port, 'localhost', 3) + + ++@unittest.skipUnless(threading, 'Threading required for this test.') ++class TooLongLineTests(unittest.TestCase): ++ respdata = '250 OK' + ('.' * smtplib._MAXLINE * 2) + '\n' ++ ++ def setUp(self): ++ self.old_stdout = sys.stdout ++ self.output = StringIO.StringIO() ++ sys.stdout = self.output ++ ++ self.evt = threading.Event() ++ self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) ++ self.sock.settimeout(15) ++ self.port = test_support.bind_port(self.sock) ++ servargs = (self.evt, self.respdata, self.sock) ++ threading.Thread(target=server, args=servargs).start() ++ self.evt.wait() ++ self.evt.clear() ++ ++ def tearDown(self): ++ self.evt.wait() ++ sys.stdout = self.old_stdout ++ ++ def testLineTooLong(self): ++ self.assertRaises(smtplib.SMTPResponseException, smtplib.SMTP, ++ HOST, self.port, 'localhost', 3) ++ ++ + sim_users = {'Mr.A@somewhere.com':'John A', + 'Ms.B@somewhere.com':'Sally B', + 'Mrs.C@somewhereesle.com':'Ruth C', +@@ -526,7 +553,8 @@ class SMTPSimTests(unittest.TestCase): + def test_main(verbose=None): + test_support.run_unittest(GeneralTests, DebuggingServerTests, + NonConnectingTests, +- BadHELOServerTests, SMTPSimTests) ++ BadHELOServerTests, SMTPSimTests, ++ TooLongLineTests) + + if __name__ == '__main__': + test_main() +diff --git a/Lib/httplib.py b/Lib/httplib.py +--- a/Lib/httplib.py ++++ b/Lib/httplib.py +@@ -215,6 +215,10 @@ MAXAMOUNT = 1048576 + # maximal line length when calling readline(). + _MAXLINE = 65536 + ++# maximum amount of headers accepted ++_MAXHEADERS = 100 ++ ++ + class HTTPMessage(mimetools.Message): + + def addheader(self, key, value): +@@ -271,6 +275,8 @@ class HTTPMessage(mimetools.Message): + elif self.seekable: + tell = self.fp.tell + while True: ++ if len(hlist) > _MAXHEADERS: ++ raise HTTPException("got more than %d headers" % _MAXHEADERS) + if tell: + try: + startofline = tell() +diff --git a/Lib/test/test_httplib.py b/Lib/test/test_httplib.py +--- a/Lib/test/test_httplib.py ++++ b/Lib/test/test_httplib.py +@@ -262,6 +262,13 @@ class BasicTest(TestCase): + if resp.read() != "": + self.fail("Did not expect response from HEAD request") + ++ def test_too_many_headers(self): ++ headers = '\r\n'.join('Header%d: foo' % i for i in xrange(200)) + '\r\n' ++ text = ('HTTP/1.1 200 OK\r\n' + headers) ++ s = FakeSocket(text) ++ r = httplib.HTTPResponse(s) ++ self.assertRaises(httplib.HTTPException, r.begin) ++ + def test_send_file(self): + expected = 'GET /foo HTTP/1.1\r\nHost: example.com\r\n' \ + 'Accept-Encoding: identity\r\nContent-Length:' diff --git a/jython.spec b/jython.spec index 1472a5e..e2050ec 100644 --- a/jython.spec +++ b/jython.spec @@ -9,7 +9,7 @@ Name: jython Version: 2.7 -Release: 0.6.rc2%{?dist} +Release: 0.7.rc2%{?dist} Summary: A Java implementation of the Python language License: ASL 1.1 and BSD and CNRI and JPython and Python URL: http://www.jython.org/ @@ -24,6 +24,9 @@ Patch0: jython-cachedir.patch # Avoid rebuilding and validating poms when installing maven stuff and don't gpg sign Patch1: jython-dont-validate-pom.patch +# Fix for CVE-2013-1752, see https://bugzilla.redhat.com/show_bug.cgi?id=1159201 +Patch2: jython-CVE-2013-1752.patch + Requires: python >= %{cpython_version} Requires: antlr32-java Requires: apache-commons-compress @@ -99,6 +102,10 @@ Demonstrations and samples for %{name}. %patch0 %patch1 +pushd lib-python/2.7 +%patch2 -p2 +popd + # Set correct encoding for source to fix javadoc generation sed -i -e '723i encoding="UTF-8"' build.xml @@ -196,6 +203,9 @@ EOF %{_datadir}/%{name}/Demo %changelog +* Mon Apr 13 2015 Mat Booth - 2.7-0.7.rc2 +- Fix CVE-2013-1752 - multiple unbound readline() DoS flaws in python stdlib + * Thu Apr 09 2015 Mat Booth - 2.7-0.6.rc2 - BR/R jnr-posix >= 3.0.9