Skip to content

Commit

Permalink
Add a ZeroLengthHeaderError raised if header name is 0-length
Browse files Browse the repository at this point in the history
This is to mitigate CVE-2019-9516, 0-Length Headers Leak. It will
allow hpack users, such as hyper-h2, to close connections if this
happens on the basis that the client is likely attempting a DoS
attack.
  • Loading branch information
pgjones committed Aug 26, 2019
1 parent ec8c671 commit feee8be
Show file tree
Hide file tree
Showing 5 changed files with 45 additions and 4 deletions.
12 changes: 12 additions & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@ Release History

**API Changes (Backward Compatible)**

**Security Fixes**

- CVE-2019-9516: 0-Length Headers Leak. This release now enforces that
headers are not zero length. This is to avoid an attack whereby an
attacker sends a stream of headers with a 0-length header name in
order to consume server resources cheaply.

This also adds a ``ZeroLengthHeaderError``, which is thrown by the
``decode`` method if any header name is zero length. This is ok as
empty header names are forbidden. This places the HPACK decoder into
a broken state: it must not be used after this exception is thrown.

**Bugfixes**

- Performance improvement of static header search. Use dict search instead
Expand Down
5 changes: 3 additions & 2 deletions hpack/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@
from .hpack import Encoder, Decoder
from .struct import HeaderTuple, NeverIndexedHeaderTuple
from .exceptions import (
HPACKError, HPACKDecodingError, InvalidTableIndex, OversizedHeaderListError
HPACKError, HPACKDecodingError, InvalidTableIndex,
OversizedHeaderListError, ZeroLengthHeaderError,
)

__all__ = [
'Encoder', 'Decoder', 'HPACKError', 'HPACKDecodingError',
'InvalidTableIndex', 'HeaderTuple', 'NeverIndexedHeaderTuple',
'OversizedHeaderListError'
'OversizedHeaderListError', 'ZeroLengthHeaderError',
]

__version__ = '3.1.0dev0'
9 changes: 9 additions & 0 deletions hpack/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,12 @@ class InvalidTableSizeError(HPACKDecodingError):
.. versionadded:: 3.0.0
"""
pass


class ZeroLengthHeaderError(HPACKDecodingError):
"""
A zero length header has been received. This may be a DoS attack.
.. versionadded:: 3.1.0
"""
pass
11 changes: 10 additions & 1 deletion hpack/hpack.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
from .table import HeaderTable, table_entry_size
from .compat import to_byte, to_bytes
from .exceptions import (
HPACKDecodingError, OversizedHeaderListError, InvalidTableSizeError
HPACKDecodingError, OversizedHeaderListError, InvalidTableSizeError,
ZeroLengthHeaderError,
)
from .huffman import HuffmanEncoder
from .huffman_constants import (
Expand Down Expand Up @@ -496,6 +497,7 @@ def decode(self, data, raw=False):

if header:
headers.append(header)
self._assert_valid_header_size(header)
inflated_size += table_entry_size(*header)

if inflated_size > self.max_header_list_size:
Expand All @@ -516,6 +518,13 @@ def decode(self, data, raw=False):
except UnicodeDecodeError:
raise HPACKDecodingError("Unable to decode headers as UTF-8.")

def _assert_valid_header_size(self, header):
"""
Check that the header size is valid, i.e. non-zero.
"""
if len(header[0]) == 0:
raise ZeroLengthHeaderError()

def _assert_valid_table_size(self):
"""
Check that the table size set by the encoder is lower than the maximum
Expand Down
12 changes: 11 additions & 1 deletion test/test_hpack.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from hpack.hpack import Encoder, Decoder, _dict_to_iterable, _to_bytes
from hpack.exceptions import (
HPACKDecodingError, InvalidTableIndex, OversizedHeaderListError,
InvalidTableSizeError
InvalidTableSizeError, ZeroLengthHeaderError,
)
from hpack.struct import HeaderTuple, NeverIndexedHeaderTuple
import itertools
Expand Down Expand Up @@ -638,6 +638,16 @@ def test_max_header_list_size(self):
with pytest.raises(OversizedHeaderListError):
d.decode(data)

def test_zero_length_header(self):
"""
If a header has a name of zero length it is invalid and the HPACK
decoder raises a ZeroLengthHeaderError.
"""
d = Decoder(max_header_list_size=44)
data = b"@\x80\x80"
with pytest.raises(ZeroLengthHeaderError):
d.decode(data)

def test_can_decode_multiple_header_table_size_changes(self):
"""
If multiple header table size changes are sent in at once, they are
Expand Down

0 comments on commit feee8be

Please sign in to comment.