Permalink
Browse files

Merge pull request #3366 from nateprewitt/2947-no-returns-in-header

check for headers containing return characters
  • Loading branch information...
2 parents ebadf81 + 2669ab7 commit bd9e8f22715448258a3cf21d3bdf3bcb72573b8b @Lukasa Lukasa committed Jul 2, 2016
Showing with 75 additions and 7 deletions.
  1. +1 −0 AUTHORS.rst
  2. +5 −1 requests/exceptions.py
  3. +8 −4 requests/models.py
  4. +19 −1 requests/utils.py
  5. +42 −1 tests/test_requests.py
View
@@ -166,3 +166,4 @@ Patches and Suggestions
- Dmitry Dygalo (`@Stranger6667 <https://github.com/Stranger6667>`_)
- piotrjurkiewicz
- Jesse Shapiro <jesse@jesseshapiro.net> (`@haikuginger <https://github.com/haikuginger>`_)
+- Nate Prewitt <nate.prewitt@gmail.com> (`@nateprewitt <https://github.com/nateprewitt>`_)
View
@@ -80,7 +80,11 @@ class InvalidSchema(RequestException, ValueError):
class InvalidURL(RequestException, ValueError):
- """ The URL provided was somehow invalid. """
+ """The URL provided was somehow invalid."""
+
+
+class InvalidHeader(RequestException, ValueError):
+ """The header value provided was somehow invalid."""
class ChunkedEncodingError(RequestException):
View
@@ -27,7 +27,8 @@
from .utils import (
guess_filename, get_auth_from_url, requote_uri,
stream_decode_response_unicode, to_key_val_list, parse_header_links,
- iter_slices, guess_json_utf, super_len, to_native_string)
+ iter_slices, guess_json_utf, super_len, to_native_string,
+ check_header_validity)
from .compat import (
cookielib, urlunparse, urlsplit, urlencode, str, bytes, StringIO,
is_py2, chardet, builtin_str, basestring)
@@ -403,10 +404,13 @@ def prepare_url(self, url, params):
def prepare_headers(self, headers):
"""Prepares the given HTTP headers."""
+ self.headers = CaseInsensitiveDict()
if headers:
- self.headers = CaseInsensitiveDict((to_native_string(name), value) for name, value in headers.items())
- else:
- self.headers = CaseInsensitiveDict()
+ for header in headers.items():
+ # Raise exception on invalid header value.
+ check_header_validity(header)
+ name, value = header
+ self.headers[to_native_string(name)] = value
def prepare_body(self, data, files, json=None):
"""Prepares the given HTTP body data."""
View
@@ -27,7 +27,7 @@
basestring)
from .cookies import RequestsCookieJar, cookiejar_from_dict
from .structures import CaseInsensitiveDict
-from .exceptions import InvalidURL, FileModeWarning
+from .exceptions import InvalidURL, InvalidHeader, FileModeWarning
_hush_pyflakes = (RequestsCookieJar,)
@@ -732,6 +732,24 @@ def to_native_string(string, encoding='ascii'):
return out
+# Moved outside of function to avoid recompile every call
+_CLEAN_HEADER_REGEX_BYTE = re.compile(b'^\\S[^\\r\\n]*$|^$')
+_CLEAN_HEADER_REGEX_STR = re.compile(r'^\S[^\r\n]*$|^$')
+
+def check_header_validity(header):
+ """Verifies that header value doesn't contain leading whitespace or
+ return characters. This prevents unintended header injection.
+
+ :param header: tuple, in the format (name, value).
+ """
+ name, value = header
+
+ if isinstance(value, bytes):
+ pat = _CLEAN_HEADER_REGEX_BYTE
+ else:
+ pat = _CLEAN_HEADER_REGEX_STR
+ if not pat.match(value):
+ raise InvalidHeader("Invalid return character or leading space in header: %s" % name)
def urldefragauth(url):
"""
View
@@ -23,7 +23,7 @@
from requests.exceptions import (
ConnectionError, ConnectTimeout, InvalidSchema, InvalidURL,
MissingSchema, ReadTimeout, Timeout, RetryError, TooManyRedirects,
- ProxyError)
+ ProxyError, InvalidHeader)
from requests.models import PreparedRequest
from requests.structures import CaseInsensitiveDict
from requests.sessions import SessionRedirectMixin
@@ -1133,6 +1133,47 @@ def test_header_keys_are_native(self, httpbin):
assert 'unicode' in p.headers.keys()
assert 'byte' in p.headers.keys()
+ def test_header_validation(self,httpbin):
+ """Ensure prepare_headers regex isn't flagging valid header contents."""
+ headers_ok = {'foo': 'bar baz qux',
+ 'bar': '1',
+ 'baz': '',
+ 'qux': str.encode(u'fbbq')}
+ r = requests.get(httpbin('get'), headers=headers_ok)
+ assert r.request.headers['foo'] == headers_ok['foo']
+
+ def test_header_no_return_chars(self, httpbin):
+ """Ensure that a header containing return character sequences raise an
+ exception. Otherwise, multiple headers are created from single string.
+ """
+ headers_ret = {'foo': 'bar\r\nbaz: qux'}
+ headers_lf = {'foo': 'bar\nbaz: qux'}
+ headers_cr = {'foo': 'bar\rbaz: qux'}
+
+ # Test for newline
+ with pytest.raises(InvalidHeader):
+ r = requests.get(httpbin('get'), headers=headers_ret)
+ # Test for line feed
+ with pytest.raises(InvalidHeader):
+ r = requests.get(httpbin('get'), headers=headers_lf)
+ # Test for carriage return
+ with pytest.raises(InvalidHeader):
+ r = requests.get(httpbin('get'), headers=headers_cr)
+
+ def test_header_no_leading_space(self, httpbin):
+ """Ensure headers containing leading whitespace raise
+ InvalidHeader Error before sending.
+ """
+ headers_space = {'foo': ' bar'}
+ headers_tab = {'foo': ' bar'}
+
+ # Test for whitespace
+ with pytest.raises(InvalidHeader):
+ r = requests.get(httpbin('get'), headers=headers_space)
+ # Test for tab
+ with pytest.raises(InvalidHeader):
+ r = requests.get(httpbin('get'), headers=headers_tab)
+
@pytest.mark.parametrize('files', ('foo', b'foo', bytearray(b'foo')))
def test_can_send_objects_with_files(self, httpbin, files):
data = {'a': 'this is a string'}

0 comments on commit bd9e8f2

Please sign in to comment.