Skip to content

Commit 02234a2

Browse files
committed
Merge pull request #204 from python-hyper/issue/194
Automatically prevent some headers from being indexed.
2 parents 2cdc7dd + dd27b7b commit 02234a2

File tree

4 files changed

+323
-2
lines changed

4 files changed

+323
-2
lines changed

HISTORY.rst

+2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ Bugfixes
2424
~~~~~~~~
2525

2626
- Correctly forbid pseudo-headers that were not defined in RFC 7540.
27+
- Automatically ensure that all ``Authorization`` headers and short ``Cookie``
28+
headers are prevented from being added to encoding contexts.
2729

2830
2.2.3 (2016-04-13)
2931
------------------

h2/stream.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
ProtocolError, StreamClosedError, InvalidBodyLengthError
2525
)
2626
from .utilities import (
27-
guard_increment_window, is_informational_response, authority_from_headers
27+
guard_increment_window, is_informational_response, authority_from_headers,
28+
secure_headers
2829
)
2930

3031

@@ -988,8 +989,10 @@ def _build_headers_frames(self,
988989
"""
989990
Helper method to build headers or push promise frames.
990991
"""
991-
# We need to lowercase the header names.
992+
# We need to lowercase the header names, and to ensure that secure
993+
# header fields are kept out of compression contexts.
992994
headers = _lowercase_header_names(headers)
995+
headers = secure_headers(headers)
993996
encoded_headers = encoder.encode(headers)
994997

995998
# Slice into blocks of max_outbound_frame_size. Be careful with this:

h2/utilities.py

+28
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
"""
88
import re
99

10+
from hpack import NeverIndexedHeaderTuple
11+
1012
from .exceptions import ProtocolError, FlowControlError
1113

1214
UPPER_RE = re.compile(b"[A-Z]")
@@ -31,6 +33,32 @@
3133
])
3234

3335

36+
def secure_headers(headers):
37+
"""
38+
Certain headers are at risk of being attacked during the header compression
39+
phase, and so need to be kept out of header compression contexts. This
40+
function automatically transforms certain specific headers into HPACK
41+
never-indexed fields to ensure they don't get added to header compression
42+
contexts.
43+
44+
This function currently implements two rules:
45+
46+
- All 'authorization' headers are automatically made never-indexed.
47+
- Any 'cookie' header field shorter than 20 bytes long is made
48+
never-indexed.
49+
50+
These two fields are the most at-risk. These rules are inspired by Firefox
51+
and nghttp2.
52+
"""
53+
for header in headers:
54+
if header[0] in (b'authorization', u'authorization'):
55+
yield NeverIndexedHeaderTuple(*header)
56+
elif header[0] in (b'cookie', u'cookie') and len(header[1]) < 20:
57+
yield NeverIndexedHeaderTuple(*header)
58+
else:
59+
yield header
60+
61+
3462
def is_informational_response(headers):
3563
"""
3664
Searches a header block for a :status header to confirm that a given

test/test_header_indexing.py

+288
Original file line numberDiff line numberDiff line change
@@ -311,3 +311,291 @@ def test_header_tuples_are_decoded_push_promise(self,
311311

312312
assert isinstance(event, h2.events.PushedStreamReceived)
313313
assert_header_blocks_actually_equal(headers, event.headers)
314+
315+
316+
class TestSecureHeaders(object):
317+
"""
318+
Certain headers should always be transformed to their never-indexed form.
319+
"""
320+
example_request_headers = [
321+
(u':authority', u'example.com'),
322+
(u':path', u'/'),
323+
(u':scheme', u'https'),
324+
(u':method', u'GET'),
325+
]
326+
bytes_example_request_headers = [
327+
(b':authority', b'example.com'),
328+
(b':path', b'/'),
329+
(b':scheme', b'https'),
330+
(b':method', b'GET'),
331+
]
332+
possible_auth_headers = [
333+
(u'authorization', u'test'),
334+
(u'Authorization', u'test'),
335+
(u'authorization', u'really long test'),
336+
HeaderTuple(u'authorization', u'test'),
337+
HeaderTuple(u'Authorization', u'test'),
338+
HeaderTuple(u'authorization', u'really long test'),
339+
NeverIndexedHeaderTuple(u'authorization', u'test'),
340+
NeverIndexedHeaderTuple(u'Authorization', u'test'),
341+
NeverIndexedHeaderTuple(u'authorization', u'really long test'),
342+
(b'authorization', b'test'),
343+
(b'Authorization', b'test'),
344+
(b'authorization', b'really long test'),
345+
HeaderTuple(b'authorization', b'test'),
346+
HeaderTuple(b'Authorization', b'test'),
347+
HeaderTuple(b'authorization', b'really long test'),
348+
NeverIndexedHeaderTuple(b'authorization', b'test'),
349+
NeverIndexedHeaderTuple(b'Authorization', b'test'),
350+
NeverIndexedHeaderTuple(b'authorization', b'really long test'),
351+
]
352+
secured_cookie_headers = [
353+
(u'cookie', u'short'),
354+
(u'Cookie', u'short'),
355+
(u'cookie', u'nineteen byte cooki'),
356+
HeaderTuple(u'cookie', u'short'),
357+
HeaderTuple(u'Cookie', u'short'),
358+
HeaderTuple(u'cookie', u'nineteen byte cooki'),
359+
NeverIndexedHeaderTuple(u'cookie', u'short'),
360+
NeverIndexedHeaderTuple(u'Cookie', u'short'),
361+
NeverIndexedHeaderTuple(u'cookie', u'nineteen byte cooki'),
362+
NeverIndexedHeaderTuple(u'cookie', u'longer manually secured cookie'),
363+
(b'cookie', b'short'),
364+
(b'Cookie', b'short'),
365+
(b'cookie', b'nineteen byte cooki'),
366+
HeaderTuple(b'cookie', b'short'),
367+
HeaderTuple(b'Cookie', b'short'),
368+
HeaderTuple(b'cookie', b'nineteen byte cooki'),
369+
NeverIndexedHeaderTuple(b'cookie', b'short'),
370+
NeverIndexedHeaderTuple(b'Cookie', b'short'),
371+
NeverIndexedHeaderTuple(b'cookie', b'nineteen byte cooki'),
372+
NeverIndexedHeaderTuple(b'cookie', b'longer manually secured cookie'),
373+
]
374+
unsecured_cookie_headers = [
375+
(u'cookie', u'twenty byte cookie!!'),
376+
(u'Cookie', u'twenty byte cookie!!'),
377+
(u'cookie', u'substantially longer than 20 byte cookie'),
378+
HeaderTuple(u'cookie', u'twenty byte cookie!!'),
379+
HeaderTuple(u'cookie', u'twenty byte cookie!!'),
380+
HeaderTuple(u'Cookie', u'twenty byte cookie!!'),
381+
(b'cookie', b'twenty byte cookie!!'),
382+
(b'Cookie', b'twenty byte cookie!!'),
383+
(b'cookie', b'substantially longer than 20 byte cookie'),
384+
HeaderTuple(b'cookie', b'twenty byte cookie!!'),
385+
HeaderTuple(b'cookie', b'twenty byte cookie!!'),
386+
HeaderTuple(b'Cookie', b'twenty byte cookie!!'),
387+
]
388+
389+
@pytest.mark.parametrize(
390+
'headers', (example_request_headers, bytes_example_request_headers)
391+
)
392+
@pytest.mark.parametrize('auth_header', possible_auth_headers)
393+
def test_authorization_headers_never_indexed(self,
394+
headers,
395+
auth_header,
396+
frame_factory):
397+
"""
398+
Authorization headers are always forced to be never-indexed, regardless
399+
of their form.
400+
"""
401+
# Regardless of what we send, we expect it to be never indexed.
402+
send_headers = headers + [auth_header]
403+
expected_headers = headers + [
404+
NeverIndexedHeaderTuple(auth_header[0].lower(), auth_header[1])
405+
]
406+
407+
c = h2.connection.H2Connection()
408+
c.initiate_connection()
409+
410+
# Clear the data, then send headers.
411+
c.clear_outbound_data_buffer()
412+
c.send_headers(1, send_headers)
413+
414+
f = frame_factory.build_headers_frame(headers=expected_headers)
415+
assert c.data_to_send() == f.serialize()
416+
417+
@pytest.mark.parametrize(
418+
'headers', (example_request_headers, bytes_example_request_headers)
419+
)
420+
@pytest.mark.parametrize('auth_header', possible_auth_headers)
421+
def test_authorization_headers_never_indexed_push(self,
422+
headers,
423+
auth_header,
424+
frame_factory):
425+
"""
426+
Authorization headers are always forced to be never-indexed, regardless
427+
of their form, when pushed by a server.
428+
"""
429+
# Regardless of what we send, we expect it to be never indexed.
430+
send_headers = headers + [auth_header]
431+
expected_headers = headers + [
432+
NeverIndexedHeaderTuple(auth_header[0].lower(), auth_header[1])
433+
]
434+
435+
c = h2.connection.H2Connection(client_side=False)
436+
c.receive_data(frame_factory.preamble())
437+
438+
# We can use normal headers for the request.
439+
f = frame_factory.build_headers_frame(
440+
self.example_request_headers
441+
)
442+
c.receive_data(f.serialize())
443+
444+
frame_factory.refresh_encoder()
445+
expected_frame = frame_factory.build_push_promise_frame(
446+
stream_id=1,
447+
promised_stream_id=2,
448+
headers=expected_headers,
449+
flags=['END_HEADERS'],
450+
)
451+
452+
c.clear_outbound_data_buffer()
453+
c.push_stream(
454+
stream_id=1,
455+
promised_stream_id=2,
456+
request_headers=send_headers
457+
)
458+
459+
assert c.data_to_send() == expected_frame.serialize()
460+
461+
@pytest.mark.parametrize(
462+
'headers', (example_request_headers, bytes_example_request_headers)
463+
)
464+
@pytest.mark.parametrize('cookie_header', secured_cookie_headers)
465+
def test_short_cookie_headers_never_indexed(self,
466+
headers,
467+
cookie_header,
468+
frame_factory):
469+
"""
470+
Short cookie headers, and cookies provided as NeverIndexedHeaderTuple,
471+
are never indexed.
472+
"""
473+
# Regardless of what we send, we expect it to be never indexed.
474+
send_headers = headers + [cookie_header]
475+
expected_headers = headers + [
476+
NeverIndexedHeaderTuple(cookie_header[0].lower(), cookie_header[1])
477+
]
478+
479+
c = h2.connection.H2Connection()
480+
c.initiate_connection()
481+
482+
# Clear the data, then send headers.
483+
c.clear_outbound_data_buffer()
484+
c.send_headers(1, send_headers)
485+
486+
f = frame_factory.build_headers_frame(headers=expected_headers)
487+
assert c.data_to_send() == f.serialize()
488+
489+
@pytest.mark.parametrize(
490+
'headers', (example_request_headers, bytes_example_request_headers)
491+
)
492+
@pytest.mark.parametrize('cookie_header', secured_cookie_headers)
493+
def test_short_cookie_headers_never_indexed_push(self,
494+
headers,
495+
cookie_header,
496+
frame_factory):
497+
"""
498+
Short cookie headers, and cookies provided as NeverIndexedHeaderTuple,
499+
are never indexed when pushed by servers.
500+
"""
501+
# Regardless of what we send, we expect it to be never indexed.
502+
send_headers = headers + [cookie_header]
503+
expected_headers = headers + [
504+
NeverIndexedHeaderTuple(cookie_header[0].lower(), cookie_header[1])
505+
]
506+
507+
c = h2.connection.H2Connection(client_side=False)
508+
c.receive_data(frame_factory.preamble())
509+
510+
# We can use normal headers for the request.
511+
f = frame_factory.build_headers_frame(
512+
self.example_request_headers
513+
)
514+
c.receive_data(f.serialize())
515+
516+
frame_factory.refresh_encoder()
517+
expected_frame = frame_factory.build_push_promise_frame(
518+
stream_id=1,
519+
promised_stream_id=2,
520+
headers=expected_headers,
521+
flags=['END_HEADERS'],
522+
)
523+
524+
c.clear_outbound_data_buffer()
525+
c.push_stream(
526+
stream_id=1,
527+
promised_stream_id=2,
528+
request_headers=send_headers
529+
)
530+
531+
assert c.data_to_send() == expected_frame.serialize()
532+
533+
@pytest.mark.parametrize(
534+
'headers', (example_request_headers, bytes_example_request_headers)
535+
)
536+
@pytest.mark.parametrize('cookie_header', unsecured_cookie_headers)
537+
def test_long_cookie_headers_can_be_indexed(self,
538+
headers,
539+
cookie_header,
540+
frame_factory):
541+
"""
542+
Longer cookie headers can be indexed.
543+
"""
544+
# Regardless of what we send, we expect it to be indexed.
545+
send_headers = headers + [cookie_header]
546+
expected_headers = headers + [
547+
HeaderTuple(cookie_header[0].lower(), cookie_header[1])
548+
]
549+
550+
c = h2.connection.H2Connection()
551+
c.initiate_connection()
552+
553+
# Clear the data, then send headers.
554+
c.clear_outbound_data_buffer()
555+
c.send_headers(1, send_headers)
556+
557+
f = frame_factory.build_headers_frame(headers=expected_headers)
558+
assert c.data_to_send() == f.serialize()
559+
560+
@pytest.mark.parametrize(
561+
'headers', (example_request_headers, bytes_example_request_headers)
562+
)
563+
@pytest.mark.parametrize('cookie_header', unsecured_cookie_headers)
564+
def test_long_cookie_headers_can_be_indexed_push(self,
565+
headers,
566+
cookie_header,
567+
frame_factory):
568+
"""
569+
Longer cookie headers can be indexed.
570+
"""
571+
# Regardless of what we send, we expect it to be never indexed.
572+
send_headers = headers + [cookie_header]
573+
expected_headers = headers + [
574+
HeaderTuple(cookie_header[0].lower(), cookie_header[1])
575+
]
576+
577+
c = h2.connection.H2Connection(client_side=False)
578+
c.receive_data(frame_factory.preamble())
579+
580+
# We can use normal headers for the request.
581+
f = frame_factory.build_headers_frame(
582+
self.example_request_headers
583+
)
584+
c.receive_data(f.serialize())
585+
586+
frame_factory.refresh_encoder()
587+
expected_frame = frame_factory.build_push_promise_frame(
588+
stream_id=1,
589+
promised_stream_id=2,
590+
headers=expected_headers,
591+
flags=['END_HEADERS'],
592+
)
593+
594+
c.clear_outbound_data_buffer()
595+
c.push_stream(
596+
stream_id=1,
597+
promised_stream_id=2,
598+
request_headers=send_headers
599+
)
600+
601+
assert c.data_to_send() == expected_frame.serialize()

0 commit comments

Comments
 (0)