Skip to content

Commit a112a8a

Browse files
Issue #22928: Disabled HTTP header injections in http.client.
Original patch by Demian Brecht.
1 parent c775ad6 commit a112a8a

File tree

3 files changed

+97
-0
lines changed

3 files changed

+97
-0
lines changed

Lib/http/client.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
import email.message
7171
import io
7272
import os
73+
import re
7374
import socket
7475
import collections
7576
from urllib.parse import urlsplit
@@ -217,6 +218,34 @@
217218
_MAXLINE = 65536
218219
_MAXHEADERS = 100
219220

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

221250
class HTTPMessage(email.message.Message):
222251
# XXX The only usage of this method is in
@@ -1060,12 +1089,20 @@ def putheader(self, header, *values):
10601089

10611090
if hasattr(header, 'encode'):
10621091
header = header.encode('ascii')
1092+
1093+
if not _is_legal_header_name(header):
1094+
raise ValueError('Invalid header name %r' % (header,))
1095+
10631096
values = list(values)
10641097
for i, one_value in enumerate(values):
10651098
if hasattr(one_value, 'encode'):
10661099
values[i] = one_value.encode('latin-1')
10671100
elif isinstance(one_value, int):
10681101
values[i] = str(one_value).encode('ascii')
1102+
1103+
if _is_illegal_header_value(values[i]):
1104+
raise ValueError('Invalid header value %r' % (values[i],))
1105+
10691106
value = b'\r\n\t'.join(values)
10701107
header = header + b': ' + value
10711108
self._output(header)

Lib/test/test_httplib.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,33 @@ def test_putheader(self):
148148
conn.putheader('Content-length', 42)
149149
self.assertIn(b'Content-length: 42', conn._buffer)
150150

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

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

181238
class BasicTest(TestCase):
182239
def test_status_lines(self):

Misc/NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ Core and Builtins
1818
Library
1919
-------
2020

21+
- Issue #22928: Disabled HTTP header injections in http.client.
22+
Original patch by Demian Brecht.
23+
2124
- Issue #23615: Modules bz2, tarfile and tokenize now can be reloaded with
2225
imp.reload(). Patch by Thomas Kluyver.
2326

0 commit comments

Comments
 (0)