Repository: Kinto/kinto

Issue:
Crash in decode_header with invalid continuation byte
From prod
```
Traceback (most recent call last):
  File \"/data/kinto-dist/lib/python2.7/site-packages/pyramid/tweens.py\", line 22, in excview_tween
    response = handler(request)
  File \"/data/kinto-dist/lib/python2.7/site-packages/pyramid_tm/__init__.py\", line 119, in tm_tween
    reraise(*exc_info)
  File \"/data/kinto-dist/lib/python2.7/site-packages/pyramid_tm/__init__.py\", line 98, in tm_tween
    response = handler(request)
  File \"/data/kinto-dist/lib/python2.7/site-packages/pyramid/router.py\", line 158, in handle_request
    view_name
  File \"/data/kinto-dist/lib/python2.7/site-packages/pyramid/view.py\", line 547, in _call_view
    response = view_callable(context, request)
  File \"/data/kinto-dist/lib/python2.7/site-packages/pyramid/config/views.py\", line 182, in __call__
    return view(context, request)
  File \"/data/kinto-dist/lib/python2.7/site-packages/pyramid/viewderivers.py\", line 393, in attr_view
    return view(context, request)
  File \"/data/kinto-dist/lib/python2.7/site-packages/pyramid/viewderivers.py\", line 371, in predicate_wrapper
    return view(context, request)
  File \"/data/kinto-dist/lib/python2.7/site-packages/pyramid/viewderivers.py\", line 302, in _secured_view
    return view(context, request)
  File \"/data/kinto-dist/lib/python2.7/site-packages/pyramid/viewderivers.py\", line 442, in rendered_view
    result = view(context, request)
  File \"/data/kinto-dist/lib/python2.7/site-packages/pyramid/viewderivers.py\", line 147, in _requestonly_view
    response = view(request)
  File \"/data/kinto-dist/lib/python2.7/site-packages/cornice/service.py\", line 571, in wrapper
    response = view_()
  File \"/data/kinto-dist/lib/python2.7/site-packages/kinto/views/records.py\", line 78, in collection_get
    result = super(Record, self).collection_get()
  File \"/data/kinto-dist/lib/python2.7/site-packages/kinto/core/resource/__init__.py\", line 249, in collection_get
    self._raise_304_if_not_modified()
  File \"/data/kinto-dist/lib/python2.7/site-packages/kinto/core/resource/__init__.py\", line 744, in _raise_304_if_not_modified
    if_none_match = decode_header(if_none_match)
  File \"/data/kinto-dist/lib/python2.7/site-packages/kinto/core/utils.py\", line 394, in decode_header
    value = value.decode(encoding)
  File \"/data/kinto-dist/lib64/python2.7/encodings/utf_8.py\", line 16, in decode
    return codecs.utf_8_decode(input, errors, True)
    UnicodeDecodeError: \'utf8\' codec can\'t decode byte 0xea in position 15: invalid continuation 
```


Discussion:

Resolving patch:
diff --git a/kinto/core/resource/__init__.py b/kinto/core/resource/__init__.py
--- a/kinto/core/resource/__init__.py
+++ b/kinto/core/resource/__init__.py
@@ -786,7 +786,15 @@ def _raise_304_if_not_modified(self, record=None):
         if not if_none_match:
             return
 
-        if_none_match = decode_header(if_none_match)
+        error_details = {
+            'location': 'header',
+            'description': "Invalid value for If-None-Match"
+        }
+
+        try:
+            if_none_match = decode_header(if_none_match)
+        except UnicodeDecodeError:
+            raise_invalid(self.request, **error_details)
 
         try:
             if not (if_none_match[0] == if_none_match[-1] == '"'):
@@ -795,10 +803,6 @@ def _raise_304_if_not_modified(self, record=None):
         except (IndexError, ValueError):
             if if_none_match == '*':
                 return
-            error_details = {
-                'location': 'header',
-                'description': "Invalid value for If-None-Match"
-            }
             raise_invalid(self.request, **error_details)
 
         if record:
@@ -824,9 +828,18 @@ def _raise_412_if_modified(self, record=None):
         if not if_match and not if_none_match:
             return
 
-        if_match = decode_header(if_match) if if_match else None
+        error_details = {
+            'location': 'header',
+            'description': ("Invalid value for If-Match. The value should "
+                            "be integer between double quotes.")}
 
-        if record and if_none_match and decode_header(if_none_match) == '*':
+        try:
+            if_match = decode_header(if_match) if if_match else None
+            if_none_match = decode_header(if_none_match) if if_none_match else None
+        except UnicodeDecodeError:
+            raise_invalid(self.request, **error_details)
+
+        if record and if_none_match == '*':
             if record.get(self.model.deleted_field, False):
                 # Tombstones should not prevent creation.
                 return
@@ -837,12 +850,6 @@ def _raise_412_if_modified(self, record=None):
                     raise ValueError()
                 modified_since = int(if_match[1:-1])
             except (IndexError, ValueError):
-                message = ("Invalid value for If-Match. The value should "
-                           "be integer between double quotes.")
-                error_details = {
-                    'location': 'header',
-                    'description': message
-                }
                 raise_invalid(self.request, **error_details)
         else:
             # In case _raise_304_if_not_modified() did not raise.


Test patch:
diff --git a/tests/core/resource/test_preconditions.py b/tests/core/resource/test_preconditions.py
--- a/tests/core/resource/test_preconditions.py
+++ b/tests/core/resource/test_preconditions.py
@@ -53,6 +53,11 @@ def test_if_none_match_empty_raises_invalid(self):
         self.assertRaises(httpexceptions.HTTPBadRequest,
                           self.resource.collection_get)
 
+    def test_if_none_match_bad_unicode_raises_invalid(self):
+        self.resource.request.headers['If-None-Match'] = b'utf8 \xe9'
+        self.assertRaises(httpexceptions.HTTPBadRequest,
+                          self.resource.collection_get)
+
     def test_if_none_match_without_quotes_raises_invalid(self):
         self.resource.request.headers['If-None-Match'] = '1234'
         self.assertRaises(httpexceptions.HTTPBadRequest,
@@ -250,3 +255,8 @@ def test_if_match_not_integer_raises_invalid(self):
         self.resource.request.headers['If-Match'] = '"abc"'
         self.assertRaises(httpexceptions.HTTPBadRequest,
                           self.resource.collection_get)
+
+    def test_if_match_bad_unicode_raises_invalid(self):
+        self.resource.request.headers['If-Match'] = b'utf8 \xe9'
+        self.assertRaises(httpexceptions.HTTPBadRequest,
+                          self.resource.collection_get)