Repository: Kinto/kinto

Issue:
Crash in Postgresql with empty last_modified
The last_modified value is supposed to be validated before reaching the backend...

```
(psycopg2.DataError) invalid input syntax for integer: ""
LINE 7:                AND as_epoch(last_modified) > ''
                                                     ^
 [SQL: '\n        WITH total_filtered AS (\n            SELECT COUNT(id) AS count\n              FROM records\n             WHERE parent_id = %(parent_id)s\n               AND collection_id = %(collection_id)s\n               AND as_epoch(last_modified) > %(filters_value_0)s\n        ),\n        collection_filtered AS (\n            SELECT id, last_modified, data\n              FROM records\n             WHERE parent_id = %(parent_id)s\n               AND collection_id = %(collection_id)s\n               AND as_epoch(last_modified) > %(filters_value_0)s\n        ),\n        fake_deleted AS (\n            SELECT (%(deleted_field)s)::JSONB AS data\n        ),\n        filtered_deleted AS (\n            SELECT id, last_modified, fake_deleted.data AS data\n              FROM deleted, fake_deleted\n             WHERE parent_id = %(parent_id)s\n               AND collection_id = %(collection_id)s\n               AND as_epoch(last_modified) > %(filters_value_0)s\n               \n        ),\n        all_records AS (\n            SELECT * FROM filtered_deleted\n             UNION ALL\n            SELECT * FROM collection_filtered\n        ),\n        paginated_records AS (\n            SELECT DISTINCT id\n              FROM all_records\n              \n        )\n        SELECT total_filtered.count AS count_total,\n               a.id, as_epoch(a.last_modified) AS last_modified, a.data\n          FROM paginated_records AS p JOIN all_records AS a ON (a.id = p.id),\n               total_filtered\n          ORDER BY last_modified DESC, last_modified DESC\n          LIMIT %(pagination_limit)s;\n        '] [parameters: {'deleted_field': '{"deleted":true}', 'collection_id': 'record', 'parent_id': '/buckets/blocklists/collections/addons', 'filters_value_0': '', 'pagination_limit': 10000}]
```
Crash in Postgresql with empty last_modified
The last_modified value is supposed to be validated before reaching the backend...

```
(psycopg2.DataError) invalid input syntax for integer: ""
LINE 7:                AND as_epoch(last_modified) > ''
                                                     ^
 [SQL: '\n        WITH total_filtered AS (\n            SELECT COUNT(id) AS count\n              FROM records\n             WHERE parent_id = %(parent_id)s\n               AND collection_id = %(collection_id)s\n               AND as_epoch(last_modified) > %(filters_value_0)s\n        ),\n        collection_filtered AS (\n            SELECT id, last_modified, data\n              FROM records\n             WHERE parent_id = %(parent_id)s\n               AND collection_id = %(collection_id)s\n               AND as_epoch(last_modified) > %(filters_value_0)s\n        ),\n        fake_deleted AS (\n            SELECT (%(deleted_field)s)::JSONB AS data\n        ),\n        filtered_deleted AS (\n            SELECT id, last_modified, fake_deleted.data AS data\n              FROM deleted, fake_deleted\n             WHERE parent_id = %(parent_id)s\n               AND collection_id = %(collection_id)s\n               AND as_epoch(last_modified) > %(filters_value_0)s\n               \n        ),\n        all_records AS (\n            SELECT * FROM filtered_deleted\n             UNION ALL\n            SELECT * FROM collection_filtered\n        ),\n        paginated_records AS (\n            SELECT DISTINCT id\n              FROM all_records\n              \n        )\n        SELECT total_filtered.count AS count_total,\n               a.id, as_epoch(a.last_modified) AS last_modified, a.data\n          FROM paginated_records AS p JOIN all_records AS a ON (a.id = p.id),\n               total_filtered\n          ORDER BY last_modified DESC, last_modified DESC\n          LIMIT %(pagination_limit)s;\n        '] [parameters: {'deleted_field': '{"deleted":true}', 'collection_id': 'record', 'parent_id': '/buckets/blocklists/collections/addons', 'filters_value_0': '', 'pagination_limit': 10000}]
```


Discussion:
```
(psycopg2.DataError) invalid input syntax for integer: ""
LINE 7: ...onfirm(65534)' = '["",""]') AND as_epoch(last_modified) > ''
                                                                     ^
 [SQL: '\n        WITH total_filtered AS (\n            SELECT COUNT(id) AS count\n              FROM records\n             WHERE parent_id = %(parent_id)s\n               AND collection_id = %(collection_id)s\n               AND (data->%(filters_field_0_0)s IS NOT NULL AND data->%(filters_field_0_0)s = %(filters_value_0)s) AND as_epoch(last_modified) > %(filters_value_1)s\n        ),\n        collection_filtered AS (\n            SELECT id, last_modified, data\n              FROM records\n             WHERE parent_id = %(parent_id)s\n               AND collection_id = %(collection_id)s\n               AND (data->%(filters_field_0_0)s IS NOT NULL AND data->%(filters_field_0_0)s = %(filters_value_0)s) AND as_epoch(last_modified) > %(filters_value_1)s\n        ),\n        fake_deleted AS (\n            SELECT (%(deleted_field)s)::JSONB AS data\n        ),\n        filtered_deleted AS (\n            SELECT id, last_modified, fake_deleted.data AS data\n              FROM deleted, fake_deleted\n             WHERE parent_id = %(parent_id)s\n               AND collection_id = %(collection_id)s\n               AND (data->%(filters_field_0_0)s IS NOT NULL AND data->%(filters_field_0_0)s = %(filters_value_0)s) AND as_epoch(last_modified) > %(filters_value_1)s\n               \n        ),\n        all_records AS (\n            SELECT * FROM filtered_deleted\n             UNION ALL\n            SELECT * FROM collection_filtered\n        ),\n        paginated_records AS (\n            SELECT DISTINCT id\n              FROM all_records\n              \n        )\n        SELECT total_filtered.count AS count_total,\n               a.id, as_epoch(a.last_modified) AS last_modified, a.data\n          FROM paginated_records AS p JOIN all_records AS a ON (a.id = p.id),\n               total_filtered\n          ORDER BY last_modified DESC\n          LIMIT %(pagination_limit)s;\n        '] [parameters: {'collection_id': 'record', 'filters_field_0_0': 'confirm(65534)', 'pagination_limit': 10000, 'parent_id': '/buckets/blocklists/collections/plugins', 'filters_value_1': '', 'filters_value_0': '["",""]', 'deleted_field': '{"deleted":true}'}]
```
```
(psycopg2.DataError) invalid input syntax for integer: ""
LINE 7: ...onfirm(65534)' = '["",""]') AND as_epoch(last_modified) > ''
                                                                     ^
 [SQL: '\n        WITH total_filtered AS (\n            SELECT COUNT(id) AS count\n              FROM records\n             WHERE parent_id = %(parent_id)s\n               AND collection_id = %(collection_id)s\n               AND (data->%(filters_field_0_0)s IS NOT NULL AND data->%(filters_field_0_0)s = %(filters_value_0)s) AND as_epoch(last_modified) > %(filters_value_1)s\n        ),\n        collection_filtered AS (\n            SELECT id, last_modified, data\n              FROM records\n             WHERE parent_id = %(parent_id)s\n               AND collection_id = %(collection_id)s\n               AND (data->%(filters_field_0_0)s IS NOT NULL AND data->%(filters_field_0_0)s = %(filters_value_0)s) AND as_epoch(last_modified) > %(filters_value_1)s\n        ),\n        fake_deleted AS (\n            SELECT (%(deleted_field)s)::JSONB AS data\n        ),\n        filtered_deleted AS (\n            SELECT id, last_modified, fake_deleted.data AS data\n              FROM deleted, fake_deleted\n             WHERE parent_id = %(parent_id)s\n               AND collection_id = %(collection_id)s\n               AND (data->%(filters_field_0_0)s IS NOT NULL AND data->%(filters_field_0_0)s = %(filters_value_0)s) AND as_epoch(last_modified) > %(filters_value_1)s\n               \n        ),\n        all_records AS (\n            SELECT * FROM filtered_deleted\n             UNION ALL\n            SELECT * FROM collection_filtered\n        ),\n        paginated_records AS (\n            SELECT DISTINCT id\n              FROM all_records\n              \n        )\n        SELECT total_filtered.count AS count_total,\n               a.id, as_epoch(a.last_modified) AS last_modified, a.data\n          FROM paginated_records AS p JOIN all_records AS a ON (a.id = p.id),\n               total_filtered\n          ORDER BY last_modified DESC\n          LIMIT %(pagination_limit)s;\n        '] [parameters: {'collection_id': 'record', 'filters_field_0_0': 'confirm(65534)', 'pagination_limit': 10000, 'parent_id': '/buckets/blocklists/collections/plugins', 'filters_value_1': '', 'filters_value_0': '["",""]', 'deleted_field': '{"deleted":true}'}]
```

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
@@ -952,6 +952,10 @@ def _extract_filters(self):
                                'parameters')
                         send_alert(self.request, message, url)
                     operator = COMPARISON.LT
+
+                if value == '':
+                    raise_invalid(self.request, **error_details)
+
                 filters.append(
                     Filter(self.model.modified_field, value, operator)
                 )
@@ -982,6 +986,9 @@ def _extract_filters(self):
                 if has_invalid_value:
                     raise_invalid(self.request, **error_details)
 
+            if field == self.model.modified_field and value == '':
+                raise_invalid(self.request, **error_details)
+
             filters.append(Filter(field, value, operator))
 
         return filters


Test patch:
diff --git a/tests/core/resource/test_filter.py b/tests/core/resource/test_filter.py
--- a/tests/core/resource/test_filter.py
+++ b/tests/core/resource/test_filter.py
@@ -68,6 +68,17 @@ def test_filter_on_unknown_attribute_raises_error(self):
         self.assertRaises(httpexceptions.HTTPBadRequest,
                           self.resource.collection_get)
 
+    def test_filter_raises_error_if_last_modified_value_is_empty(self):
+        self.validated['querystring'] = {'last_modified': ''}
+        self.assertRaises(httpexceptions.HTTPBadRequest,
+                          self.resource.collection_get)
+        self.validated['querystring'] = {'_since': ''}
+        self.assertRaises(httpexceptions.HTTPBadRequest,
+                          self.resource.collection_get)
+        self.validated['querystring'] = {'lt_last_modified': ''}
+        self.assertRaises(httpexceptions.HTTPBadRequest,
+                          self.resource.collection_get)
+
     def test_filter_errors_are_json_formatted(self):
         self.patch_known_field.stop()
         self.validated['querystring'] = {'foo': 1}