Skip to content

Commit 59bdf63

Browse files
Issue #22928: Disabled HTTP header injections in httplib.
Original patch by Demian Brecht.
1 parent 205408d commit 59bdf63

File tree

3 files changed

+98
-1
lines changed

3 files changed

+98
-1
lines changed

‎Lib/httplib.py‎

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868

6969
from array import array
7070
import os
71+
import re
7172
import socket
7273
from sys import py3kwarning
7374
from urlparse import urlsplit
@@ -218,6 +219,34 @@
218219
# maximum amount of headers accepted
219220
_MAXHEADERS = 100
220221

222+
# Header name/value ABNF (http://tools.ietf.org/html/rfc7230#section-3.2)
223+
#
224+
# VCHAR = %x21-7E
225+
# obs-text = %x80-FF
226+
# header-field = field-name ":" OWS field-value OWS
227+
# field-name = token
228+
# field-value = *( field-content / obs-fold )
229+
# field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
230+
# field-vchar = VCHAR / obs-text
231+
#
232+
# obs-fold = CRLF 1*( SP / HTAB )
233+
# ; obsolete line folding
234+
# ; see Section 3.2.4
235+
236+
# token = 1*tchar
237+
#
238+
# tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*"
239+
# / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
240+
# / DIGIT / ALPHA
241+
# ; any VCHAR, except delimiters
242+
#
243+
# VCHAR defined in http://tools.ietf.org/html/rfc5234#appendix-B.1
244+
245+
# the patterns for both name and value are more leniant than RFC
246+
# definitions to allow for backwards compatibility
247+
_is_legal_header_name = re.compile(r'\A[^:\s][^:\r\n]*\Z').match
248+
_is_illegal_header_value = re.compile(r'\n(?![ \t])|\r(?![ \t\n])').search
249+
221250

222251
class HTTPMessage(mimetools.Message):
223252

@@ -983,7 +1012,16 @@ def putheader(self, header, *values):
9831012
if self.__state != _CS_REQ_STARTED:
9841013
raise CannotSendHeader()
9851014

986-
hdr = '%s: %s' % (header, '\r\n\t'.join([str(v) for v in values]))
1015+
header = '%s' % header
1016+
if not _is_legal_header_name(header):
1017+
raise ValueError('Invalid header name %r' % (header,))
1018+
1019+
values = [str(v) for v in values]
1020+
for one_value in values:
1021+
if _is_illegal_header_value(one_value):
1022+
raise ValueError('Invalid header value %r' % (one_value,))
1023+
1024+
hdr = '%s: %s' % (header, '\r\n\t'.join(values))
9871025
self._output(hdr)
9881026

9891027
def endheaders(self, message_body=None):

‎Lib/test/test_httplib.py‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,33 @@ def test_putheader(self):
145145
conn.putheader('Content-length',42)
146146
self.assertIn('Content-length: 42', conn._buffer)
147147

148+
conn.putheader('Foo', ' bar ')
149+
self.assertIn(b'Foo: bar ', conn._buffer)
150+
conn.putheader('Bar', '\tbaz\t')
151+
self.assertIn(b'Bar: \tbaz\t', conn._buffer)
152+
conn.putheader('Authorization', 'Bearer mytoken')
153+
self.assertIn(b'Authorization: Bearer mytoken', conn._buffer)
154+
conn.putheader('IterHeader', 'IterA', 'IterB')
155+
self.assertIn(b'IterHeader: IterA\r\n\tIterB', conn._buffer)
156+
conn.putheader('LatinHeader', b'\xFF')
157+
self.assertIn(b'LatinHeader: \xFF', conn._buffer)
158+
conn.putheader('Utf8Header', b'\xc3\x80')
159+
self.assertIn(b'Utf8Header: \xc3\x80', conn._buffer)
160+
conn.putheader('C1-Control', b'next\x85line')
161+
self.assertIn(b'C1-Control: next\x85line', conn._buffer)
162+
conn.putheader('Embedded-Fold-Space', 'is\r\n allowed')
163+
self.assertIn(b'Embedded-Fold-Space: is\r\n allowed', conn._buffer)
164+
conn.putheader('Embedded-Fold-Tab', 'is\r\n\tallowed')
165+
self.assertIn(b'Embedded-Fold-Tab: is\r\n\tallowed', conn._buffer)
166+
conn.putheader('Key Space', 'value')
167+
self.assertIn(b'Key Space: value', conn._buffer)
168+
conn.putheader('KeySpace ', 'value')
169+
self.assertIn(b'KeySpace : value', conn._buffer)
170+
conn.putheader(b'Nonbreak\xa0Space', 'value')
171+
self.assertIn(b'Nonbreak\xa0Space: value', conn._buffer)
172+
conn.putheader(b'\xa0NonbreakSpace', 'value')
173+
self.assertIn(b'\xa0NonbreakSpace: value', conn._buffer)
174+
148175
def test_ipv6host_header(self):
149176
# Default host header on IPv6 transaction should wrapped by [] if
150177
# its actual IPv6 address
@@ -174,6 +201,35 @@ def test_malformed_headers_coped_with(self):
174201
self.assertEqual(resp.getheader('First'), 'val')
175202
self.assertEqual(resp.getheader('Second'), 'val')
176203

204+
def test_invalid_headers(self):
205+
conn = httplib.HTTPConnection('example.com')
206+
conn.sock = FakeSocket('')
207+
conn.putrequest('GET', '/')
208+
209+
# http://tools.ietf.org/html/rfc7230#section-3.2.4, whitespace is no
210+
# longer allowed in header names
211+
cases = (
212+
(b'Invalid\r\nName', b'ValidValue'),
213+
(b'Invalid\rName', b'ValidValue'),
214+
(b'Invalid\nName', b'ValidValue'),
215+
(b'\r\nInvalidName', b'ValidValue'),
216+
(b'\rInvalidName', b'ValidValue'),
217+
(b'\nInvalidName', b'ValidValue'),
218+
(b' InvalidName', b'ValidValue'),
219+
(b'\tInvalidName', b'ValidValue'),
220+
(b'Invalid:Name', b'ValidValue'),
221+
(b':InvalidName', b'ValidValue'),
222+
(b'ValidName', b'Invalid\r\nValue'),
223+
(b'ValidName', b'Invalid\rValue'),
224+
(b'ValidName', b'Invalid\nValue'),
225+
(b'ValidName', b'InvalidValue\r\n'),
226+
(b'ValidName', b'InvalidValue\r'),
227+
(b'ValidName', b'InvalidValue\n'),
228+
)
229+
for name, value in cases:
230+
with self.assertRaisesRegexp(ValueError, 'Invalid header'):
231+
conn.putheader(name, value)
232+
177233

178234
class BasicTest(TestCase):
179235
def test_status_lines(self):

‎Misc/NEWS‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ Core and Builtins
2121
Library
2222
-------
2323

24+
- Issue #22928: Disabled HTTP header injections in httplib.
25+
Original patch by Demian Brecht.
26+
2427
- Issue #23615: Module tarfile is now can be reloaded with imp.reload().
2528

2629
- Issue #22853: Fixed a deadlock when use multiprocessing.Queue at import time.

0 commit comments

Comments
 (0)