CVE-2016-9964: Improper Neutralization of CRLF Sequences ('CRLF Injection') (CWE-93)
value: 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.
CWE 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.
CVSS v3 base score: 6.5
Published: 2016-12-16T09:59Z
Repository: https://github.com/bottlepy/bottle
Language: Python
Files changed: bottle.py, test/test_environ.py
Commit date: 2016-12-10 14:10:26 +0100
Commit message: 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.

Diff: diff --git a/bottle.py b/bottle.py
index 5f28dda..62c00f4 100755
--- a/bottle.py
+++ b/bottle.py
@@ -1571,13 +1571,20 @@ class BaseRequest(object):
             del self.environ['bottle.request.ext.%s' % name]
         except KeyError:
             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=''):
         self.name, self.default = name, default
         self.reader, self.writer = reader, writer
@@ -2168,11 +2175,10 @@ class FormsDict(MultiDict):
         # Without this guard, pickle generates a cryptic TypeError:
         if name.startswith('__') and name.endswith('__'):
             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. """
 
     def __init__(self, *a, **ka):
@@ -2187,29 +2193,26 @@ class HeaderDict(MultiDict):
 
     def __getitem__(self, key):
         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 []
 
     def get(self, key, default=None, index=-1):
         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]
 
 
 class WSGIHeaderDict(DictMixin):
diff --git a/test/test_environ.py b/test/test_environ.py
index 2312ce4..4ee1111 100755
--- a/test/test_environ.py
+++ b/test/test_environ.py
@@ -1,10 +1,13 @@
 # -*- coding: utf-8 -*-
 ''' Tests for the BaseRequest and BaseResponse objects and their subclasses. '''
 
 import unittest
 import sys
+
+import itertools
+
 import bottle
 from bottle import request, tob, touni, tonat, json_dumps, HTTPError, parse_date
 from test import tools
 import wsgiref.util
 import base64
@@ -693,10 +696,21 @@ class TestResponse(unittest.TestCase):
         response['x-test'] = 5
         self.assertEqual('5', response['x-test'])
         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()
         now = datetime.datetime.now()
         response.expires = now

Vulnerable code: def _hkey(s):
    return s.title().replace('_', '-')


        self.dict[_hkey(key)] = [value if isinstance(value, unicode) else
                                 str(value)]
        self.dict.setdefault(_hkey(key), []).append(
            value if isinstance(value, unicode) else str(value))
        self.dict[_hkey(key)] = [value if isinstance(value, unicode) else
                                 str(value)]
        for name in [_hkey(n) for n in names]:
Fixed code: 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
        self.dict[_hkey(key)] = [_hval(value)]
        self.dict.setdefault(_hkey(key), []).append(_hval(value))
        self.dict[_hkey(key)] = [_hval(value)]
        for name in (_hkey(n) for n in names):

import itertools

    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)