We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Batch requests accept additional attributes
We should probably forbid them.

```
> echo '{"requests": [], "foo": {}}' | http post http://localhost:8888/v1/batch 

HTTP/1.1 200 OK
Access-Control-Expose-Headers: Retry-After, Alert, Content-Length, Backoff
Content-Length: 16
Content-Type: application/json
Date: Thu, 16 Mar 2017 23:01:01 GMT
Server: waitress
X-Content-Type-Options: nosniff

{
    "responses": []
}
```

Edit: I just notice we currently test for batch ignoring additional attributes. Is this a feature?

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `kinto/core/views/batch.py`
Content:
```
1 import logging
2 
3 import colander
4 from cornice.validators import colander_validator
5 from pyramid import httpexceptions
6 from pyramid.security import NO_PERMISSION_REQUIRED
7 
8 from kinto.core import errors
9 from kinto.core import Service
10 from kinto.core.errors import ErrorSchema
11 from kinto.core.utils import merge_dicts, build_request, build_response
12 
13 
14 subrequest_logger = logging.getLogger("subrequest.summary")
15 
16 valid_http_method = colander.OneOf(('GET', 'HEAD', 'DELETE', 'TRACE',
17                                     'POST', 'PUT', 'PATCH'))
18 
19 
20 def string_values(node, cstruct):
21     """Validate that a ``colander.Mapping`` only has strings in its values.
22 
23     .. warning::
24 
25         Should be associated to a ``colander.Mapping`` schema node.
26     """
27     are_strings = [isinstance(v, str) for v in cstruct.values()]
28     if not all(are_strings):
29         error_msg = '{} contains non string value'.format(cstruct)
30         raise colander.Invalid(node, error_msg)
31 
32 
33 class BatchRequestSchema(colander.MappingSchema):
34     method = colander.SchemaNode(colander.String(),
35                                  validator=valid_http_method,
36                                  missing=colander.drop)
37     path = colander.SchemaNode(colander.String(),
38                                validator=colander.Regex('^/'))
39     headers = colander.SchemaNode(colander.Mapping(unknown='preserve'),
40                                   validator=string_values,
41                                   missing=colander.drop)
42     body = colander.SchemaNode(colander.Mapping(unknown='preserve'),
43                                missing=colander.drop)
44 
45 
46 class BatchPayloadSchema(colander.MappingSchema):
47     defaults = BatchRequestSchema(missing=colander.drop).clone()
48     requests = colander.SchemaNode(colander.Sequence(),
49                                    BatchRequestSchema())
50 
51     def __init__(self, *args, **kwargs):
52         super().__init__(*args, **kwargs)
53         # On defaults, path is not mandatory.
54         self.get('defaults').get('path').missing = colander.drop
55 
56     def deserialize(self, cstruct=colander.null):
57         """Preprocess received data to carefully merge defaults.
58         """
59         if cstruct is not colander.null:
60             defaults = cstruct.get('defaults')
61             requests = cstruct.get('requests')
62             if isinstance(defaults, dict) and isinstance(requests, list):
63                 for request in requests:
64                     if isinstance(request, dict):
65                         merge_dicts(request, defaults)
66         return super().deserialize(cstruct)
67 
68 
69 class BatchRequest(colander.MappingSchema):
70     body = BatchPayloadSchema()
71 
72 
73 class BatchResponseSchema(colander.MappingSchema):
74     status = colander.SchemaNode(colander.Integer())
75     path = colander.SchemaNode(colander.String())
76     headers = colander.SchemaNode(colander.Mapping(unknown='preserve'),
77                                   validator=string_values,
78                                   missing=colander.drop)
79     body = colander.SchemaNode(colander.Mapping(unknown='preserve'),
80                                missing=colander.drop)
81 
82 
83 class BatchResponseBodySchema(colander.MappingSchema):
84     responses = colander.SequenceSchema(BatchResponseSchema(missing=colander.drop))
85 
86 
87 class BatchResponse(colander.MappingSchema):
88     body = BatchResponseBodySchema()
89 
90 
91 class ErrorResponseSchema(colander.MappingSchema):
92     body = ErrorSchema()
93 
94 
95 batch_responses = {
96     '200': BatchResponse(description='Return a list of operation responses.'),
97     '400': ErrorResponseSchema(description='The request was badly formatted.'),
98     'default': ErrorResponseSchema(description='an unknown error occurred.')
99 }
100 
101 batch = Service(name="batch", path='/batch',
102                 description="Batch operations")
103 
104 
105 @batch.post(schema=BatchRequest,
106             validators=(colander_validator,),
107             permission=NO_PERMISSION_REQUIRED,
108             tags=['Batch'], operation_id='batch',
109             response_schemas=batch_responses)
110 def post_batch(request):
111     requests = request.validated['body']['requests']
112 
113     request.log_context(batch_size=len(requests))
114 
115     limit = request.registry.settings['batch_max_requests']
116     if limit and len(requests) > int(limit):
117         error_msg = 'Number of requests is limited to {}'.format(limit)
118         request.errors.add('body', 'requests', error_msg)
119         return
120 
121     if any([batch.path in req['path'] for req in requests]):
122         error_msg = 'Recursive call on {} endpoint is forbidden.'.format(batch.path)
123         request.errors.add('body', 'requests', error_msg)
124         return
125 
126     responses = []
127 
128     for subrequest_spec in requests:
129         subrequest = build_request(request, subrequest_spec)
130 
131         log_context = {'path': subrequest.path,
132                        'method': subrequest.method,
133                        **request.log_context()}
134         try:
135             # Invoke subrequest without individual transaction.
136             resp, subrequest = request.follow_subrequest(subrequest,
137                                                          use_tweens=False)
138         except httpexceptions.HTTPException as e:
139             if e.content_type == 'application/json':
140                 resp = e
141             else:
142                 # JSONify raw Pyramid errors.
143                 resp = errors.http_error(e)
144 
145         subrequest_logger.info('subrequest.summary', extra=log_context)
146 
147         dict_resp = build_response(resp, subrequest)
148         responses.append(dict_resp)
149 
150     return {
151         'responses': responses
152     }
153
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/kinto/core/views/batch.py b/kinto/core/views/batch.py
--- a/kinto/core/views/batch.py
+++ b/kinto/core/views/batch.py
@@ -42,12 +42,20 @@
     body = colander.SchemaNode(colander.Mapping(unknown='preserve'),
                                missing=colander.drop)
 
+    @staticmethod
+    def schema_type():
+        return colander.Mapping(unknown='raise')
+
 
 class BatchPayloadSchema(colander.MappingSchema):
     defaults = BatchRequestSchema(missing=colander.drop).clone()
     requests = colander.SchemaNode(colander.Sequence(),
                                    BatchRequestSchema())
 
+    @staticmethod
+    def schema_type():
+        return colander.Mapping(unknown='raise')
+
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
         # On defaults, path is not mandatory.

{"golden_diff": "diff --git a/kinto/core/views/batch.py b/kinto/core/views/batch.py\n--- a/kinto/core/views/batch.py\n+++ b/kinto/core/views/batch.py\n@@ -42,12 +42,20 @@\n     body = colander.SchemaNode(colander.Mapping(unknown='preserve'),\n                                missing=colander.drop)\n \n+    @staticmethod\n+    def schema_type():\n+        return colander.Mapping(unknown='raise')\n+\n \n class BatchPayloadSchema(colander.MappingSchema):\n     defaults = BatchRequestSchema(missing=colander.drop).clone()\n     requests = colander.SchemaNode(colander.Sequence(),\n                                    BatchRequestSchema())\n \n+    @staticmethod\n+    def schema_type():\n+        return colander.Mapping(unknown='raise')\n+\n     def __init__(self, *args, **kwargs):\n         super().__init__(*args, **kwargs)\n         # On defaults, path is not mandatory.\n", "issue": "Batch requests accept additional attributes\nWe should probably forbid them.\r\n\r\n```\r\n> echo '{\"requests\": [], \"foo\": {}}' | http post http://localhost:8888/v1/batch \r\n\r\nHTTP/1.1 200 OK\r\nAccess-Control-Expose-Headers: Retry-After, Alert, Content-Length, Backoff\r\nContent-Length: 16\r\nContent-Type: application/json\r\nDate: Thu, 16 Mar 2017 23:01:01 GMT\r\nServer: waitress\r\nX-Content-Type-Options: nosniff\r\n\r\n{\r\n    \"responses\": []\r\n}\r\n```\r\n\r\nEdit: I just notice we currently test for batch ignoring additional attributes. Is this a feature?\n", "before_files": [{"content": "import logging\n\nimport colander\nfrom cornice.validators import colander_validator\nfrom pyramid import httpexceptions\nfrom pyramid.security import NO_PERMISSION_REQUIRED\n\nfrom kinto.core import errors\nfrom kinto.core import Service\nfrom kinto.core.errors import ErrorSchema\nfrom kinto.core.utils import merge_dicts, build_request, build_response\n\n\nsubrequest_logger = logging.getLogger(\"subrequest.summary\")\n\nvalid_http_method = colander.OneOf(('GET', 'HEAD', 'DELETE', 'TRACE',\n                                    'POST', 'PUT', 'PATCH'))\n\n\ndef string_values(node, cstruct):\n    \"\"\"Validate that a ``colander.Mapping`` only has strings in its values.\n\n    .. warning::\n\n        Should be associated to a ``colander.Mapping`` schema node.\n    \"\"\"\n    are_strings = [isinstance(v, str) for v in cstruct.values()]\n    if not all(are_strings):\n        error_msg = '{} contains non string value'.format(cstruct)\n        raise colander.Invalid(node, error_msg)\n\n\nclass BatchRequestSchema(colander.MappingSchema):\n    method = colander.SchemaNode(colander.String(),\n                                 validator=valid_http_method,\n                                 missing=colander.drop)\n    path = colander.SchemaNode(colander.String(),\n                               validator=colander.Regex('^/'))\n    headers = colander.SchemaNode(colander.Mapping(unknown='preserve'),\n                                  validator=string_values,\n                                  missing=colander.drop)\n    body = colander.SchemaNode(colander.Mapping(unknown='preserve'),\n                               missing=colander.drop)\n\n\nclass BatchPayloadSchema(colander.MappingSchema):\n    defaults = BatchRequestSchema(missing=colander.drop).clone()\n    requests = colander.SchemaNode(colander.Sequence(),\n                                   BatchRequestSchema())\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        # On defaults, path is not mandatory.\n        self.get('defaults').get('path').missing = colander.drop\n\n    def deserialize(self, cstruct=colander.null):\n        \"\"\"Preprocess received data to carefully merge defaults.\n        \"\"\"\n        if cstruct is not colander.null:\n            defaults = cstruct.get('defaults')\n            requests = cstruct.get('requests')\n            if isinstance(defaults, dict) and isinstance(requests, list):\n                for request in requests:\n                    if isinstance(request, dict):\n                        merge_dicts(request, defaults)\n        return super().deserialize(cstruct)\n\n\nclass BatchRequest(colander.MappingSchema):\n    body = BatchPayloadSchema()\n\n\nclass BatchResponseSchema(colander.MappingSchema):\n    status = colander.SchemaNode(colander.Integer())\n    path = colander.SchemaNode(colander.String())\n    headers = colander.SchemaNode(colander.Mapping(unknown='preserve'),\n                                  validator=string_values,\n                                  missing=colander.drop)\n    body = colander.SchemaNode(colander.Mapping(unknown='preserve'),\n                               missing=colander.drop)\n\n\nclass BatchResponseBodySchema(colander.MappingSchema):\n    responses = colander.SequenceSchema(BatchResponseSchema(missing=colander.drop))\n\n\nclass BatchResponse(colander.MappingSchema):\n    body = BatchResponseBodySchema()\n\n\nclass ErrorResponseSchema(colander.MappingSchema):\n    body = ErrorSchema()\n\n\nbatch_responses = {\n    '200': BatchResponse(description='Return a list of operation responses.'),\n    '400': ErrorResponseSchema(description='The request was badly formatted.'),\n    'default': ErrorResponseSchema(description='an unknown error occurred.')\n}\n\nbatch = Service(name=\"batch\", path='/batch',\n                description=\"Batch operations\")\n\n\n@batch.post(schema=BatchRequest,\n            validators=(colander_validator,),\n            permission=NO_PERMISSION_REQUIRED,\n            tags=['Batch'], operation_id='batch',\n            response_schemas=batch_responses)\ndef post_batch(request):\n    requests = request.validated['body']['requests']\n\n    request.log_context(batch_size=len(requests))\n\n    limit = request.registry.settings['batch_max_requests']\n    if limit and len(requests) > int(limit):\n        error_msg = 'Number of requests is limited to {}'.format(limit)\n        request.errors.add('body', 'requests', error_msg)\n        return\n\n    if any([batch.path in req['path'] for req in requests]):\n        error_msg = 'Recursive call on {} endpoint is forbidden.'.format(batch.path)\n        request.errors.add('body', 'requests', error_msg)\n        return\n\n    responses = []\n\n    for subrequest_spec in requests:\n        subrequest = build_request(request, subrequest_spec)\n\n        log_context = {'path': subrequest.path,\n                       'method': subrequest.method,\n                       **request.log_context()}\n        try:\n            # Invoke subrequest without individual transaction.\n            resp, subrequest = request.follow_subrequest(subrequest,\n                                                         use_tweens=False)\n        except httpexceptions.HTTPException as e:\n            if e.content_type == 'application/json':\n                resp = e\n            else:\n                # JSONify raw Pyramid errors.\n                resp = errors.http_error(e)\n\n        subrequest_logger.info('subrequest.summary', extra=log_context)\n\n        dict_resp = build_response(resp, subrequest)\n        responses.append(dict_resp)\n\n    return {\n        'responses': responses\n    }\n", "path": "kinto/core/views/batch.py"}], "after_files": [{"content": "import logging\n\nimport colander\nfrom cornice.validators import colander_validator\nfrom pyramid import httpexceptions\nfrom pyramid.security import NO_PERMISSION_REQUIRED\n\nfrom kinto.core import errors\nfrom kinto.core import Service\nfrom kinto.core.errors import ErrorSchema\nfrom kinto.core.utils import merge_dicts, build_request, build_response\n\n\nsubrequest_logger = logging.getLogger(\"subrequest.summary\")\n\nvalid_http_method = colander.OneOf(('GET', 'HEAD', 'DELETE', 'TRACE',\n                                    'POST', 'PUT', 'PATCH'))\n\n\ndef string_values(node, cstruct):\n    \"\"\"Validate that a ``colander.Mapping`` only has strings in its values.\n\n    .. warning::\n\n        Should be associated to a ``colander.Mapping`` schema node.\n    \"\"\"\n    are_strings = [isinstance(v, str) for v in cstruct.values()]\n    if not all(are_strings):\n        error_msg = '{} contains non string value'.format(cstruct)\n        raise colander.Invalid(node, error_msg)\n\n\nclass BatchRequestSchema(colander.MappingSchema):\n    method = colander.SchemaNode(colander.String(),\n                                 validator=valid_http_method,\n                                 missing=colander.drop)\n    path = colander.SchemaNode(colander.String(),\n                               validator=colander.Regex('^/'))\n    headers = colander.SchemaNode(colander.Mapping(unknown='preserve'),\n                                  validator=string_values,\n                                  missing=colander.drop)\n    body = colander.SchemaNode(colander.Mapping(unknown='preserve'),\n                               missing=colander.drop)\n\n    @staticmethod\n    def schema_type():\n        return colander.Mapping(unknown='raise')\n\n\nclass BatchPayloadSchema(colander.MappingSchema):\n    defaults = BatchRequestSchema(missing=colander.drop).clone()\n    requests = colander.SchemaNode(colander.Sequence(),\n                                   BatchRequestSchema())\n\n    @staticmethod\n    def schema_type():\n        return colander.Mapping(unknown='raise')\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        # On defaults, path is not mandatory.\n        self.get('defaults').get('path').missing = colander.drop\n\n    def deserialize(self, cstruct=colander.null):\n        \"\"\"Preprocess received data to carefully merge defaults.\n        \"\"\"\n        if cstruct is not colander.null:\n            defaults = cstruct.get('defaults')\n            requests = cstruct.get('requests')\n            if isinstance(defaults, dict) and isinstance(requests, list):\n                for request in requests:\n                    if isinstance(request, dict):\n                        merge_dicts(request, defaults)\n        return super().deserialize(cstruct)\n\n\nclass BatchRequest(colander.MappingSchema):\n    body = BatchPayloadSchema()\n\n\nclass BatchResponseSchema(colander.MappingSchema):\n    status = colander.SchemaNode(colander.Integer())\n    path = colander.SchemaNode(colander.String())\n    headers = colander.SchemaNode(colander.Mapping(unknown='preserve'),\n                                  validator=string_values,\n                                  missing=colander.drop)\n    body = colander.SchemaNode(colander.Mapping(unknown='preserve'),\n                               missing=colander.drop)\n\n\nclass BatchResponseBodySchema(colander.MappingSchema):\n    responses = colander.SequenceSchema(BatchResponseSchema(missing=colander.drop))\n\n\nclass BatchResponse(colander.MappingSchema):\n    body = BatchResponseBodySchema()\n\n\nclass ErrorResponseSchema(colander.MappingSchema):\n    body = ErrorSchema()\n\n\nbatch_responses = {\n    '200': BatchResponse(description='Return a list of operation responses.'),\n    '400': ErrorResponseSchema(description='The request was badly formatted.'),\n    'default': ErrorResponseSchema(description='an unknown error occurred.')\n}\n\nbatch = Service(name=\"batch\", path='/batch',\n                description=\"Batch operations\")\n\n\n@batch.post(schema=BatchRequest,\n            validators=(colander_validator,),\n            permission=NO_PERMISSION_REQUIRED,\n            tags=['Batch'], operation_id='batch',\n            response_schemas=batch_responses)\ndef post_batch(request):\n    requests = request.validated['body']['requests']\n\n    request.log_context(batch_size=len(requests))\n\n    limit = request.registry.settings['batch_max_requests']\n    if limit and len(requests) > int(limit):\n        error_msg = 'Number of requests is limited to {}'.format(limit)\n        request.errors.add('body', 'requests', error_msg)\n        return\n\n    if any([batch.path in req['path'] for req in requests]):\n        error_msg = 'Recursive call on {} endpoint is forbidden.'.format(batch.path)\n        request.errors.add('body', 'requests', error_msg)\n        return\n\n    responses = []\n\n    for subrequest_spec in requests:\n        subrequest = build_request(request, subrequest_spec)\n\n        log_context = {'path': subrequest.path,\n                       'method': subrequest.method,\n                       **request.log_context()}\n        try:\n            # Invoke subrequest without individual transaction.\n            resp, subrequest = request.follow_subrequest(subrequest,\n                                                         use_tweens=False)\n        except httpexceptions.HTTPException as e:\n            if e.content_type == 'application/json':\n                resp = e\n            else:\n                # JSONify raw Pyramid errors.\n                resp = errors.http_error(e)\n\n        subrequest_logger.info('subrequest.summary', extra=log_context)\n\n        dict_resp = build_response(resp, subrequest)\n        responses.append(dict_resp)\n\n    return {\n        'responses': responses\n    }\n", "path": "kinto/core/views/batch.py"}]}