Permalink
Browse files

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.
1 parent 58882c7 commit 6d7e13da0f998820800ecb3fe9ccee4189aefb54 @defnull defnull committed Dec 10, 2016
Showing with 28 additions and 11 deletions.
  1. +14 −11 bottle.py
  2. +14 −0 test/test_environ.py
View
@@ -1573,9 +1573,16 @@ def __delattr__(self, name, value):
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 @@ def __getattr__(self, name, default=unicode()):
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 @@ 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 []
@@ -2207,7 +2210,7 @@ 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]
View
@@ -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 @@ def test_non_string_header(self):
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()

0 comments on commit 6d7e13d

Please sign in to comment.