Cornices/cornice

content_type errors are broken, when combined with require_csrf

Closed this issue · 4 comments

Bottom line up front:
Somehow, the interaction of require_csrf and content_type causes a “Bad CSRF Token” error response when passed the wrong Content-Type into views with CSRF checking disabled.

Consider the following service (fully runnable):

from wsgiref.simple_server import make_server

from cornice import Service
from pyramid.config import Configurator
from pyramid.csrf import CookieCSRFStoragePolicy
from pyramid.request import Request
from pyramid.response import Response

CONTENT_TYPE = 'application/json'
HOST = 'localhost'

service = Service(name='service', path='/', content_type=CONTENT_TYPE)


@service.post(require_csrf=False)
def post(__: Request) -> Response:
    return Response("POSTed!")


if __name__ == '__main__':
    with Configurator() as config:
        # Turn on automatic CSRF checking.
        # NOTE: Our view disables CSRF checking, so this *should* be NOOP.
        config.set_csrf_storage_policy(
            CookieCSRFStoragePolicy(domain=HOST))
        config.set_default_csrf_options(require_csrf=True)

        config.include('cornice')
        config.add_cornice_service(service)

        app = config.make_wsgi_app()
    make_server(HOST, 6543, app).serve_forever()

However, when passed the wrong Content-Type, the POST in fact fails with:

<html>
    <head>
        <title>400 Bad CSRF Token</title>
    </head>
    <body>
        <h1>400 Bad CSRF Token</h1>
  Access is denied.  This server can not verify that your cross-site request forgery token belongs to your login session.  Either you supplied the wrong cross-site request forgery token or your session no longer exists.  This may be due to session timeout or because browser is not supplying the credentials required, as can happen when the browser has cookies turned off.
        <br/>
        <br/>
check_csrf_token(): Invalid token
    </body>
</html>

Since I’ve disabled require_csrf for the POST view, there should be no circumstance under which this error pops up. When Content-Type: application/json, everything works as intended.

I tried to debug this myself inside Cornice, but I ran out of time after going pretty deep, without luck.

Thanks for this detailed bug report!

I confirm, I could reproduce the issue.

I modified your example to rely on pure-Pyramid and it works fine:

--- 458.py	2017-10-19 16:32:40.467257968 +0200
+++ 458-.py	2017-10-19 16:31:32.225185482 +0200
@@ -1,20 +1,18 @@
 from wsgiref.simple_server import make_server
 
 from cornice import Service
+from cornice.util import ContentTypePredicate
 from pyramid.config import Configurator
 from pyramid.csrf import CookieCSRFStoragePolicy
 from pyramid.request import Request
 from pyramid.response import Response
 
 CONTENT_TYPE = 'application/json'
 HOST = 'localhost'
 
-service = Service(name='service', path='/', content_type=CONTENT_TYPE)
-
-
-@service.post(require_csrf=False)
-def post(__: Request) -> Response:
+def post(request):
     return Response("POSTed!")
 
 
 if __name__ == '__main__':
@@ -24,9 +22,11 @@
         config.set_csrf_storage_policy(
             CookieCSRFStoragePolicy(domain=HOST))
         config.set_default_csrf_options(require_csrf=True)
 
-        config.include('cornice')
-        config.add_cornice_service(service)
+        config.add_view_predicate('content_type', ContentTypePredicate)
+
+        config.add_route('hello', '/')
+        config.add_view(post, route_name='hello', require_csrf=False, content_type=CONTENT_TYPE)
 
         app = config.make_wsgi_app()
     make_server(HOST, 6543, app).serve_forever()

It comes from the internal Cornice fallback view with receives the default CSRF option.
The fix in Cornice is a one-liner:

diff --git a/cornice/pyramidhook.py b/cornice/pyramidhook.py
index e2f9841..a8f2b7e 100644
--- a/cornice/pyramidhook.py
+++ b/cornice/pyramidhook.py
@@ -274,7 +274,8 @@ def register_service_views(config, service):
         # Add the fallback view last
         config.add_view(view=get_fallback_view(service),
                         route_name=route_name,
-                        permission=NO_PERMISSION_REQUIRED)
+                        permission=NO_PERMISSION_REQUIRED,
+                        require_csrf=False)
         config.commit()

The behaviour is then as excepted:

$ http POST :6543 "Content-Type: application/json"
HTTP/1.0 200 OK
Content-Length: 7
Content-Type: text/html; charset=UTF-8
Date: Thu, 19 Oct 2017 14:43:37 GMT
Server: WSGIServer/0.1 Python/2.7.12+
X-Content-Type-Options: nosniff

POSTed!
$  http POST :6543                                 
HTTP/1.0 415 Unsupported Media Type
Content-Length: 155
Content-Type: application/json
Date: Thu, 19 Oct 2017 14:43:41 GMT
Server: WSGIServer/0.1 Python/2.7.12+
X-Content-Type-Options: nosniff

{
    "errors": [
        {
            "description": "Content-Type header should be one of ['application/json']", 
            "location": "header", 
            "name": "Content-Type"
        }
    ], 
    "status": "error"
}

This breaks compatibility with the older versions of pyramid.

One of the projects I'm working on is still using Pyramid 1.5.8 and the following error occurs when I'm trying to run app tests.

Traceback (most recent call last):
  File "/home/avolkov/repos/test_proj/test_proj/tests/api/orders/test_rest_post.py", line 506, in setUp
    super(TestInternetIPTVPhonePost, self).setUp()
  File "/home/avolkov/repos/test_proj/test_proj/tests/common_setup.py", line 117, in setUp
    self.app = main(config_dict)
  File "/home/avolkov/repos/test_proj/test_proj/__init__.py", line 48, in main
    config.scan(ignore=b'test_proj.tests')
  File "/home/avolkov/.virtualenvs/test_proj/local/lib/python2.7/site-packages/pyramid/config/__init__.py", line 930, in scan
    ignore=ignore)
  File "/home/avolkov/.virtualenvs/test_proj/local/lib/python2.7/site-packages/venusian/__init__.py", line 239, in scan
    invoke(modname, name, ob)
  File "/home/avolkov/.virtualenvs/test_proj/local/lib/python2.7/site-packages/venusian/__init__.py", line 190, in invoke
    callback(self, name, ob)
  File "/home/avolkov/.virtualenvs/test_proj/local/lib/python2.7/site-packages/cornice/resource.py", line 123, in callback
    config.add_cornice_service(service)
  File "/home/avolkov/.virtualenvs/test_proj/local/lib/python2.7/site-packages/pyramid/util.py", line 535, in wrapper
    result = wrapped(self, *arg, **kw)
  File "/home/avolkov/.virtualenvs/test_proj/local/lib/python2.7/site-packages/cornice/pyramidhook.py", line 264, in register_service_views
    config.commit()
  File "/home/avolkov/.virtualenvs/test_proj/local/lib/python2.7/site-packages/pyramid/config/__init__.py", line 610, in commit
    self.action_state.execute_actions(introspector=self.introspector)
  File "/home/avolkov/.virtualenvs/test_proj/local/lib/python2.7/site-packages/pyramid/config/__init__.py", line 1048, in execute_actions
    for action in resolveConflicts(self.actions):
  File "/home/avolkov/.virtualenvs/test_proj/local/lib/python2.7/site-packages/pyramid/config/__init__.py", line 1132, in resolveConflicts
    discriminator = undefer(action['discriminator'])
  File "/home/avolkov/.virtualenvs/test_proj/local/lib/python2.7/site-packages/pyramid/registry.py", line 250, in undefer
    v = v.resolve()
  File "/home/avolkov/.virtualenvs/test_proj/local/lib/python2.7/site-packages/pyramid/registry.py", line 242, in resolve
    return self.func()
  File "/home/avolkov/.virtualenvs/test_proj/local/lib/python2.7/site-packages/pyramid/config/views.py", line 1166, in discrim_func
    order, preds, phash = predlist.make(self, **pvals)
  File "/home/avolkov/.virtualenvs/test_proj/local/lib/python2.7/site-packages/pyramid/config/util.py", line 158, in make
    raise ConfigurationError('Unknown predicate values: %r' % (kw,))
ConfigurationError: Unknown predicate values: {'require_csrf': False}

It looks like check_csrf was renamed some time ago to require_csrf some time ago -- Pylons/pyramid#2367

The changes were merged into Pyramid 1.7 -- Pylons/pyramid#2413

Is it possible to make this change backward-compatible, or add a requirement so cornice from then on depends on Pyramid 1.7 and higher? -- https://github.com/Cornices/cornice/blob/2.4.X/setup.py

Is it possible to make this change backward-compatible, or add a requirement so cornice from then on depends on Pyramid 1.7 and higher?

Yes sure @avolkov please open a PR with the changes you need!