[Pkg-mailman-hackers] CVE-2006-2941: Mailman: DoS caused by standards-breaking RFC 2231 formatted headers.

Lionel Elie Mamane lionel at mamane.lu
Tue Sep 12 08:25:15 UTC 2006


Hi,

I tried to prepare an update for CVE-2006-2941 in Mailman. This was
fixed upstream by updating the included python "email" package. But in
Mailman-in-sarge, the included email package is not used at all. I
believe it then uses the "email" package from python? This security
issue then needs to be solved in python? Is not present at all in
python's email package? I'm not sure. I'm confused.

For reference, I attach here the fix I prepared for Mailman before
noticing the included email package was not used at all.

-- 
Lionel
-------------- next part --------------
#! /bin/sh /usr/share/dpatch/dpatch-run
## 73_CVE-2006-2941.dpatch by  <lionel at mamane.lu>
##
## All lines beginning with `## DP:' are a description of the patch.
## DP: CVE-2006-2941: DoS caused by standards-breaking RFC 2231 formatted headers.

@DPATCH@
diff -urNad mailman-2.1.5~/misc/CVE-2006-2941.patch mailman-2.1.5/misc/CVE-2006-2941.patch
--- mailman-2.1.5~/misc/CVE-2006-2941.patch	1970-01-01 01:00:00.000000000 +0100
+++ mailman-2.1.5/misc/CVE-2006-2941.patch	2006-09-12 09:48:10.719700552 +0200
@@ -0,0 +1,414 @@
+diff --recursive -u email-2.5.7/email/__init__.py email-2.5.8/email/__init__.py
+--- email-2.5.7/email/__init__.py	2006-03-06 01:23:54.000000000 +0100
++++ email-2.5.8/email/__init__.py	2006-07-25 15:10:39.000000000 +0200
+@@ -3,7 +3,7 @@
+ 
+ """A package for parsing, handling, and generating email messages."""
+ 
+-__version__ = '2.5.5'
++__version__ = '2.5.5.debian1'
+ 
+ __all__ = [
+     'base64MIME',
+diff --recursive -u email-2.5.7/email/_parseaddr.py email-2.5.8/email/_parseaddr.py
+--- email-2.5.7/email/_parseaddr.py	2006-03-06 01:23:54.000000000 +0100
++++ email-2.5.8/email/_parseaddr.py	2006-06-13 05:43:49.000000000 +0200
+@@ -365,6 +365,7 @@
+                 break
+             elif allowcomments and self.field[self.pos] == '(':
+                 slist.append(self.getcomment())
++                continue        # have already advanced pos from getcomment
+             elif self.field[self.pos] == '\\':
+                 quote = True
+             else:
+diff --recursive -u email-2.5.7/email/test/test_email.py email-2.5.8/email/test/test_email.py
+--- email-2.5.7/email/test/test_email.py	2006-03-06 01:23:53.000000000 +0100
++++ email-2.5.8/email/test/test_email.py	2006-07-26 05:58:13.000000000 +0200
+@@ -9,7 +9,7 @@
+ import unittest
+ import warnings
+ from cStringIO import StringIO
+-from types import StringType, ListType
++from types import StringType, ListType, TupleType
+ 
+ import email
+ 
+@@ -2064,6 +2064,12 @@
+            ['foo: ;', '"Jason R. Mastaler" <jason at dom.ain>']),
+            [('', ''), ('Jason R. Mastaler', 'jason at dom.ain')])
+ 
++    def test_getaddresses_embedded_comment(self):
++        """Test proper handling of a nested comment"""
++        eq = self.assertEqual
++        addrs = Utils.getaddresses(['User ((nested comment)) <foo at bar.com>'])
++        eq(addrs[0][1], 'foo at bar.com')
++
+     def test_utils_quote_unquote(self):
+         eq = self.assertEqual
+         msg = Message()
+@@ -2750,14 +2756,17 @@
+ 
+ '''
+         msg = email.message_from_string(m)
+-        self.assertEqual(msg.get_param('NAME'),
+-                         (None, None, 'file____C__DOCUMENTS_20AND_20SETTINGS_FABIEN_LOCAL_20SETTINGS_TEMP_nsmail.htm'))
++        param = msg.get_param('NAME')
++        self.failIf(isinstance(param, TupleType))
++        self.assertEqual(
++            param,
++            'file____C__DOCUMENTS_20AND_20SETTINGS_FABIEN_LOCAL_20SETTINGS_TEMP_nsmail.htm')
+ 
+     def test_rfc2231_no_language_or_charset_in_filename(self):
+         m = '''\
+ Content-Disposition: inline;
+-\tfilename*0="This%20is%20even%20more%20";
+-\tfilename*1="%2A%2A%2Afun%2A%2A%2A%20";
++\tfilename*0*="This%20is%20even%20more%20";
++\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
+ \tfilename*2="is it not.pdf"
+ 
+ '''
+@@ -2768,8 +2777,8 @@
+     def test_rfc2231_no_language_or_charset_in_boundary(self):
+         m = '''\
+ Content-Type: multipart/alternative;
+-\tboundary*0="This%20is%20even%20more%20";
+-\tboundary*1="%2A%2A%2Afun%2A%2A%2A%20";
++\tboundary*0*="This%20is%20even%20more%20";
++\tboundary*1*="%2A%2A%2Afun%2A%2A%2A%20";
+ \tboundary*2="is it not.pdf"
+ 
+ '''
+@@ -2777,12 +2786,38 @@
+         self.assertEqual(msg.get_boundary(),
+                          'This is even more ***fun*** is it not.pdf')
+ 
++    def test_rfc2231_partly_encoded(self):
++        m = '''\
++Content-Disposition: inline;
++\tfilename*0="''This%20is%20even%20more%20";
++\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
++\tfilename*2="is it not.pdf"
++
++'''
++        msg = email.message_from_string(m)
++        self.assertEqual(
++            msg.get_filename(),
++            'This%20is%20even%20more%20***fun*** is it not.pdf') 
++
++    def test_rfc2231_partly_nonencoded(self):
++        m = '''\
++Content-Disposition: inline;
++\tfilename*0="This%20is%20even%20more%20";
++\tfilename*1="%2A%2A%2Afun%2A%2A%2A%20";
++\tfilename*2="is it not.pdf"
++
++'''
++        msg = email.message_from_string(m)
++        self.assertEqual(
++            msg.get_filename(),
++            'This%20is%20even%20more%20%2A%2A%2Afun%2A%2A%2A%20is it not.pdf') 
++
+     def test_rfc2231_no_language_or_charset_in_charset(self):
+         # This is a nonsensical charset value, but tests the code anyway
+         m = '''\
+ Content-Type: text/plain;
+-\tcharset*0="This%20is%20even%20more%20";
+-\tcharset*1="%2A%2A%2Afun%2A%2A%2A%20";
++\tcharset*0*="This%20is%20even%20more%20";
++\tcharset*1*="%2A%2A%2Afun%2A%2A%2A%20";
+ \tcharset*2="is it not.pdf"
+ 
+ '''
+@@ -2793,8 +2828,8 @@
+     def test_rfc2231_bad_encoding_in_filename(self):
+         m = '''\
+ Content-Disposition: inline;
+-\tfilename*0="bogus'xx'This%20is%20even%20more%20";
+-\tfilename*1="%2A%2A%2Afun%2A%2A%2A%20";
++\tfilename*0*="bogus'xx'This%20is%20even%20more%20";
++\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
+ \tfilename*2="is it not.pdf"
+ 
+ '''
+@@ -2825,9 +2860,9 @@
+     def test_rfc2231_bad_character_in_filename(self):
+         m = '''\
+ Content-Disposition: inline;
+-\tfilename*0="ascii'xx'This%20is%20even%20more%20";
+-\tfilename*1="%2A%2A%2Afun%2A%2A%2A%20";
+-\tfilename*2="is it not.pdf%E2"
++\tfilename*0*="ascii'xx'This%20is%20even%20more%20";
++\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
++\tfilename*2*="is it not.pdf%E2"
+ 
+ '''
+         msg = email.message_from_string(m)
+@@ -2835,6 +2870,102 @@
+                          'This is even more ***fun*** is it not.pdf\xe2')
+ 
+ 
++    def test_rfc2231_unknown_encoding(self):
++        m = """\
++Content-Transfer-Encoding: 8bit
++Content-Disposition: inline; filename*=X-UNKNOWN''myfile.txt
++
++"""
++        msg = email.message_from_string(m)
++        self.assertEqual(msg.get_filename(), 'myfile.txt')
++
++    def test_rfc2231_single_tick_in_filename_extended(self):
++        eq = self.assertEqual
++        m = """\
++Content-Type: application/x-foo;
++\tname*0*=\"Frank's\"; name*1*=\" Document\"
++
++"""
++        msg = email.message_from_string(m)
++        charset, language, s = msg.get_param('name')
++        eq(charset, None)
++        eq(language, None)
++        eq(s, "Frank's Document")
++
++    def test_rfc2231_single_tick_in_filename(self):
++        m = """\
++Content-Type: application/x-foo; name*0=\"Frank's\"; name*1=\" Document\"
++
++"""
++        msg = email.message_from_string(m)
++        param = msg.get_param('name')
++        self.failIf(isinstance(param, TupleType))
++        self.assertEqual(param, "Frank's Document")
++
++    def test_rfc2231_tick_attack_extended(self):
++        eq = self.assertEqual
++        m = """\
++Content-Type: application/x-foo;
++\tname*0*=\"us-ascii'en-us'Frank's\"; name*1*=\" Document\"
++
++"""
++        msg = email.message_from_string(m)
++        charset, language, s = msg.get_param('name')
++        eq(charset, 'us-ascii')
++        eq(language, 'en-us')
++        eq(s, "Frank's Document") 
++
++    def test_rfc2231_tick_attack(self):
++        m = """\
++Content-Type: application/x-foo;
++\tname*0=\"us-ascii'en-us'Frank's\"; name*1=\" Document\"
++
++"""
++        msg = email.message_from_string(m)
++        param = msg.get_param('name')
++        self.failIf(isinstance(param, TupleType))
++        self.assertEqual(param, "us-ascii'en-us'Frank's Document")
++
++    def test_rfc2231_no_extended_values(self):
++        eq = self.assertEqual
++        m = """\
++Content-Type: application/x-foo; name=\"Frank's Document\"
++
++"""
++        msg = email.message_from_string(m)
++        eq(msg.get_param('name'), "Frank's Document")
++
++    def test_rfc2231_encoded_then_unencoded_segments(self):
++        eq = self.assertEqual
++        m = """\
++Content-Type: application/x-foo;
++\tname*0*=\"us-ascii'en-us'My\";
++\tname*1=\" Document\";
++\tname*2*=\" For You\"
++
++"""
++        msg = email.message_from_string(m)
++        charset, language, s = msg.get_param('name')
++        eq(charset, 'us-ascii')
++        eq(language, 'en-us')
++        eq(s, 'My Document For You')
++
++    def test_rfc2231_unencoded_then_encoded_segments(self):
++        eq = self.assertEqual
++        m = """\
++Content-Type: application/x-foo;
++\tname*0=\"us-ascii'en-us'My\";
++\tname*1*=\" Document\";
++\tname*2*=\" For You\"
++
++"""
++        msg = email.message_from_string(m)
++        charset, language, s = msg.get_param('name')
++        eq(charset, 'us-ascii')
++        eq(language, 'en-us')
++        eq(s, 'My Document For You')
++
++
+ 
+ def _testclasses():
+     mod = sys.modules[__name__]
+diff --recursive -u email-2.5.7/email/Utils.py email-2.5.8/email/Utils.py
+--- email-2.5.7/email/Utils.py	2006-03-06 01:23:54.000000000 +0100
++++ email-2.5.8/email/Utils.py	2006-07-25 15:10:39.000000000 +0200
+@@ -1,14 +1,15 @@
+-# Copyright (C) 2001,2002 Python Software Foundation
+-# Author: barry at zope.com (Barry Warsaw)
++# Copyright (C) 2001-2006 Python Software Foundation
++# Author: Barry Warsaw
++# Contact: email-sig at python.org
+ 
+-"""Miscellaneous utilities.
+-"""
++"""Miscellaneous utilities."""
+ 
+ import time
+ import socket
+ import re
+ import random
+ import os
++import urllib
+ import warnings
+ from cStringIO import StringIO
+ from types import ListType
+@@ -53,6 +54,7 @@
+ EMPTYSTRING = ''
+ UEMPTYSTRING = u''
+ CRLF = '\r\n'
++TICK = "'"
+ 
+ specialsre = re.compile(r'[][\\()<>@,:;".]')
+ escapesre = re.compile(r'[][\\()"]')
+@@ -277,12 +279,14 @@
+ # RFC2231-related functions - parameter encoding and decoding
+ def decode_rfc2231(s):
+     """Decode string according to RFC 2231"""
+-    import urllib
+-    parts = s.split("'", 2)
+-    if len(parts) == 1:
++    parts = s.split(TICK, 2)
++    if len(parts) <= 2:
+         return None, None, urllib.unquote(s)
+-    charset, language, s = parts
+-    return charset, language, urllib.unquote(s)
++    if len(parts) > 3:
++        charset, language = pars[:2]
++        s = TICK.join(parts[2:])
++        return charset, language, s
++    return parts
+ 
+ 
+ def encode_rfc2231(s, charset=None, language=None):
+@@ -306,35 +310,52 @@
+ def decode_params(params):
+     """Decode parameters list according to RFC 2231.
+ 
+-    params is a sequence of 2-tuples containing (content type, string value).
++    params is a sequence of 2-tuples containing (param name, string value).
+     """
++    # Copy params so we don't mess with the original
++    params = params[:]
+     new_params = []
+-    # maps parameter's name to a list of continuations
++    # Map parameter's name to a list of continuations.  The values are a
++    # 3-tuple of the continuation number, the string value, and a flag
++    # specifying whether a particular segment is %-encoded.
+     rfc2231_params = {}
+-    # params is a sequence of 2-tuples containing (content_type, string value)
+-    name, value = params[0]
++    name, value = params.pop(0)
+     new_params.append((name, value))
+-    # Cycle through each of the rest of the parameters.
+-    for name, value in params[1:]:
++    while params:
++        name, value = params.pop(0)
++        if name.endswith('*'):
++            encoded = True
++        else:
++            encoded = False
+         value = unquote(value)
+         mo = rfc2231_continuation.match(name)
+         if mo:
+             name, num = mo.group('name', 'num')
+             if num is not None:
+                 num = int(num)
+-            rfc2231_param1 = rfc2231_params.setdefault(name, [])
+-            rfc2231_param1.append((num, value))
++            rfc2231_params.setdefault(name, []).append((num, value, encoded))
+         else:
+             new_params.append((name, '"%s"' % quote(value)))
+     if rfc2231_params:
+         for name, continuations in rfc2231_params.items():
+             value = []
++            extended = False
+             # Sort by number
+             continuations.sort()
+-            # And now append all values in num order
+-            for num, continuation in continuations:
+-                value.append(continuation)
+-            charset, language, value = decode_rfc2231(EMPTYSTRING.join(value))
+-            new_params.append(
+-                (name, (charset, language, '"%s"' % quote(value))))
++            # And now append all values in numerical order, converting
++            # %-encodings for the encoded segments.  If any of the
++            # continuation names ends in a *, then the entire string, after
++            # decoding segments and concatenating, must have the charset and
++            # language specifiers at the beginning of the string.
++            for num, s, encoded in continuations:
++                if encoded:
++                    s = urllib.unquote(s)
++                    extended = True
++                value.append(s)
++            value = quote(EMPTYSTRING.join(value))
++            if extended:
++                charset, language, value = decode_rfc2231(value)
++                new_params.append((name, (charset, language, '"%s"' % value)))
++            else:
++                new_params.append((name, '"%s"' % value))
+     return new_params
+diff --recursive -u email-2.5.7/PKG-INFO email-2.5.8/PKG-INFO
+--- email-2.5.7/PKG-INFO	2006-03-06 02:39:57.000000000 +0100
++++ email-2.5.8/PKG-INFO	2006-07-26 06:15:58.000000000 +0200
+@@ -1,10 +1,10 @@
+ Metadata-Version: 1.0
+ Name: email
+-Version: 2.5.5
++Version: 2.5.5.debian1
+ Summary: Standalone email package
+ Home-page: http://www.python.org/sigs/email-sig
+ Author: Barry Warsaw
+-Author-email: barry at python.org
++Author-email: email-sig at python.org
+ License: UNKNOWN
+ Description: UNKNOWN
+ Platform: UNKNOWN
+diff --recursive -u email-2.5.7/setup.py email-2.5.8/setup.py
+--- email-2.5.7/setup.py	2006-03-06 01:21:59.000000000 +0100
++++ email-2.5.8/setup.py	2006-07-25 16:08:43.000000000 +0200
+@@ -1,5 +1,3 @@
+-#! /usr/bin/env python
+-#
+ # Copyright (C) 2001-2006 Python Software Foundation
+ 
+ # Standard distutils setup.py install script for the `mimelib' library, a next
+@@ -20,7 +18,7 @@
+       version=email.__version__,
+       description='Standalone email package',
+       author='Barry Warsaw',
+-      author_email='barry at python.org',
++      author_email='email-sig at python.org',
+       url='http://www.python.org/sigs/email-sig',
+       packages=['email'],
+       )
+diff --recursive -u email-2.5.7/testall.py email-2.5.8/testall.py
+--- email-2.5.7/testall.py	2006-03-06 01:21:59.000000000 +0100
++++ email-2.5.8/testall.py	2006-07-25 16:09:00.000000000 +0200
+@@ -13,8 +13,8 @@
+ """
+ 
+ import sys
+-import unittest
+ import getopt
++import unittest
+ 
+ from email.test import test_email
+ 
diff -urNad mailman-2.1.5~/misc/Makefile.in mailman-2.1.5/misc/Makefile.in
--- mailman-2.1.5~/misc/Makefile.in	2004-05-14 05:34:34.000000000 +0200
+++ mailman-2.1.5/misc/Makefile.in	2006-09-12 09:39:11.682124870 +0200
@@ -90,7 +90,9 @@
 	for p in $(PACKAGES); \
 	do \
 	    gunzip -c $(srcdir)/$$p.tar.gz | (cd $(PKGDIR) ; tar xf -); \
-	    (cd $(PKGDIR)/$$p ; umask 02 ; PYTHONPATH=$(PYTHONLIBDIR) $(PYTHON) $(SETUPCMD)); \
+	    (cd $(PKGDIR)/$$p ; umask 02 ; \
+	     if [ "$$p" = "email-2.5.5" ]; then patch -p1 "$(PWD)/CVE-2006-2941.patch"; fi; \
+	     PYTHONPATH=$(PYTHONLIBDIR) $(PYTHON) $(SETUPCMD)); \
 	done
 
 finish:


More information about the Pkg-mailman-hackers mailing list