CVE-2016-9964 (published 2016-12-16T09:59Z)

redirect() in bottle.py in bottle 0.12.10 doesn\'t filter a "\\r\\n" sequence, which leads to a CRLF attack, as demonstrated by a redirect("233\\r\\nSet-Cookie: name=salt") call.

Severity: MEDIUM
CVSS v2 base score: 4.3 (AV:N/AC:M/Au:N/C:N/I:P/A:N)
CVSS v3 base score: 6.5, MEDIUM (CVSS:3.0/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N)

Weakness information:
cwe_id: CWE-93
cwe_name: Improper Neutralization of CRLF Sequences ('CRLF Injection')
description: The product uses CRLF (carriage return line feeds) as a special element, e.g. to separate lines or records, but it does not neutralize or incorrectly neutralizes CRLF sequences from inputs.
extended_description: 
            
            
         
         

Fix information:
repo_url: https://github.com/bottlepy/bottle
commit_details: repo_url: https://github.com/bottlepy/bottle
author: Marcel Hellkamp
author_date: 2016-12-10 14:10:26+01:00
msg: fix #913: Harden bottle against malformed headers.

Bottle now checks against certain control characters (\n, \r and \0) in header names or values and raises a ValueError if the application tries to set an invalid header.
repository: repo_url: https://github.com/bottlepy/bottle
repo_name: bottlepy/bottle
description: bottle.py is a fast and simple micro-framework for python web-applications.
repo_language: Python
owner: bottlepy
file_changes: filename: bottle.py
change_type: ModificationType.MODIFY
diff: @@ -1573,9 +1573,16 @@ class BaseRequest(object):
             raise AttributeError("Attribute not defined: %s" % name)
 
 
-def _hkey(s):
-    return s.title().replace('_', '-')
-
+def _hkey(key):
+    if '\n' in key or '\r' in key or '\0' in key:
+        raise ValueError("Header names must not contain control characters: %r" % key)
+    return key.title().replace('_', '-')
+
+def _hval(value):
+    value = value if isinstance(value, unicode) else str(value)
+    if '\n' in value or '\r' in value or '\0' in value:
+        raise ValueError("Header value must not contain control characters: %r" % value)
+    return value
 
 class HeaderProperty(object):
     def __init__(self, name, reader=None, writer=str, default=''):
@@ -2170,7 +2177,6 @@ class FormsDict(MultiDict):
             return super(FormsDict, self).__getattr__(name)
         return self.getunicode(name, default=default)
 
-
 class HeaderDict(MultiDict):
     """ A case-insensitive version of :class:`MultiDict` that defaults to
         replace the old value instead of appending it. """
@@ -2189,16 +2195,13 @@ class HeaderDict(MultiDict):
         return self.dict[_hkey(key)][-1]
 
     def __setitem__(self, key, value):
-        self.dict[_hkey(key)] = [value if isinstance(value, unicode) else
-                                 str(value)]
+        self.dict[_hkey(key)] = [_hval(value)]
 
     def append(self, key, value):
-        self.dict.setdefault(_hkey(key), []).append(
-            value if isinstance(value, unicode) else str(value))
+        self.dict.setdefault(_hkey(key), []).append(_hval(value))
 
     def replace(self, key, value):
-        self.dict[_hkey(key)] = [value if isinstance(value, unicode) else
-                                 str(value)]
+        self.dict[_hkey(key)] = [_hval(value)]
 
     def getall(self, key):
         return self.dict.get(_hkey(key)) or []
@@ -2207,7 +2210,7 @@ class HeaderDict(MultiDict):
         return MultiDict.get(self, _hkey(key), default, index)
 
     def filter(self, names):
-        for name in [_hkey(n) for n in names]:
+        for name in (_hkey(n) for n in names):
             if name in self.dict:
                 del self.dict[name]
 

programming_language: Python
filename: test_environ.py
change_type: ModificationType.MODIFY
diff: @@ -3,6 +3,9 @@
 
 import unittest
 import sys
+
+import itertools
+
 import bottle
 from bottle import request, tob, touni, tonat, json_dumps, HTTPError, parse_date
 from test import tools
@@ -695,6 +698,17 @@ class TestResponse(unittest.TestCase):
         response['x-test'] = None
         self.assertEqual('None', response['x-test'])
 
+    def test_prevent_control_characters_in_headers(self):
+        apis = 'append', 'replace', '__setitem__', 'setdefault'
+        masks = '{}test', 'test{}', 'te{}st'
+        tests = '\n', '\r', '\n\r', '\0'
+        for api, mask, test in itertools.product(apis, masks, tests):
+            hd = bottle.HeaderDict()
+            func = getattr(hd, api)
+            value = mask.replace("{}", test)
+            self.assertRaises(ValueError, func, value, "test-value")
+            self.assertRaises(ValueError, func, "test-name", value)
+
     def test_expires_header(self):
         import datetime
         response = BaseResponse()

programming_language: Python