Commit 54502fb8 authored by Laurent Peuch's avatar Laurent Peuch
Browse files

fix: on /rqlio, csrf is activaved only on multipart/form-data

POST for application/json are safe from csrf but not multipart/form-data.
CSRF protection is thus disabled on application/json (no matter the authentications method).

For multipart/form-data, there are 3 usecases:

1. multipart/form-data authenticated by cookies (webrowser), this requires
   csrf and this is handled by MultipartRqlIOController.
2. multipart/form-data anon user, this does not require csrf and this in
   handled by AnonMultipartRqlIOController
3. multipart/form-data authenticated with authorization, this does not
   requires csrf as there is an authentification. This is not handled here
   but in signed-request that implements the authentification.
parent 889440263e74
Pipeline #76867 passed with stages
in 2 minutes and 5 seconds
Controller that gives users rql read/ write capabilities.
Controller that gives users rql read/ write capabilities. To have token
authentication, install `cubicweb_signedrequest <>`_.
Sample usage
......@@ -11,7 +12,8 @@ Users of this service must perform a HTTP POST request to its endpoint,
that is the base url of the CubicWeb application instance appended with
the "rqlio/1.0" url path.
The posted data must use the application/json MIME type, and contain a list of
The posted data must use the `application/json` or `multipart/form-data`.
For the `application/json` MIME type, the posted data must contain a list of
pairs of the form `(rql_string, rql_args)`, where:
* `rql_string` is any valid RQL query that may contain mapping keys with
......@@ -54,4 +56,3 @@ Python client example using python-requests::
headers={'Content-Type': 'application/json'})
assert resp.status_code == 200
from cubicweb.predicates import ExpectedValuePredicate
class match_all_http_headers(ExpectedValuePredicate):
"""Return non-zero score if all HTTP headers are present"""
def __call__(self, cls, request, **kwargs):
for value in self.expected:
if not request.get_header(value):
return 0
return 1
......@@ -24,6 +24,8 @@ from cubicweb.predicates import (
from cubicweb.uilib import exc_message
from cubicweb.utils import json_dumps
......@@ -33,6 +35,8 @@ from cubicweb.web.views.urlrewrite import rgx_action, SchemaBasedRewriter
from cubicweb import Binary
from cubicweb_rqlcontroller.rql_schema_holder import RqlIOSchemaHolder
from cubicweb_rqlcontroller.predicates import match_all_http_headers
ARGRE = re.compile(r"__r(?P<ref>\d+)$")
DATARE = re.compile(r"__f(?P<ref>.+)$")
......@@ -87,7 +91,7 @@ class RqlIOSchemaController(Controller):
class RqlIOController(Controller):
class BaseRqlIOController(Controller):
"""posted rql queries and arguments use the following pattern:
[('INSERT CWUser U: U login %(login)s, U upassword %(pw)s',
......@@ -110,15 +114,9 @@ class RqlIOController(Controller):
__abstract__ = True
__regid__ = "rqlio"
__select__ = (
& match_request_content_type(
"application/json", "multipart/form-data", mode="any"
& match_form_params("version")
require_csrf = False
__select__ = match_http_method("POST") & match_form_params("version")
def json(self):
contenttype = self._cw.get_header("Content-Type", raw=False)
......@@ -186,6 +184,49 @@ class RqlIOController(Controller):
return output
class JsonRqlIOController(BaseRqlIOController):
"""RqlIOController with csrf desactivated for application/json because
application/json can't be sent through a form
__select__ = BaseRqlIOController.__select__ & match_request_content_type(
"application/json", mode="any"
require_csrf = False
class MultipartRqlIOController(BaseRqlIOController):
"""RqlIOController with csrf activated for cookie authenticated user
and multipart/form-data.
To have csrf deactivated with multipart/form-data, install
cubicweb-signedrequest and use an authentication method.
__select__ = (
& ~match_all_http_headers("Authorization") # no Authorization == cookie
& match_request_content_type("multipart/form-data", mode="any")
& authenticated_user()
class AnonMultipartRqlIOController(BaseRqlIOController):
"""RqlIOController with csrf desactivated for anonymous_user.
This allows public usage of the route.
It is expected anonymous user should have only read auhorization.
__select__ = (
& ~match_all_http_headers("Authorization")
& match_request_content_type("multipart/form-data", mode="any")
& anonymous_user()
require_csrf = False
class RQLIORewriter(SchemaBasedRewriter):
rules = [
(re.compile("/rqlio/schema"), rgx_action(controller="rqlio_schema")),
......@@ -34,7 +34,6 @@ class RqlIOTC(PyramidCWTest):
"/rqlio/%s" % version,
headers={"Content-Type": "application/json"},
def assertRQLPostKO(self, queries, reason, code=500):
......@@ -43,7 +42,6 @@ class RqlIOTC(PyramidCWTest):
headers={"Content-Type": "application/json"},
self.assertIn(reason, res_ko.json[u"reason"])
return res_ko
......@@ -107,16 +105,26 @@ class RqlIOTC(PyramidCWTest):
{"u": "__r0", "g": "__r1"},
self.webapp.login(user="admin", password=self.password)
# FIXME XXX: to change to self.webapp.login() once the issue is solved:
response = self.webapp.get("/login")
csrf_token = response.forms[0].fields["csrf_token"][0].value
res =
"/login", {"__login": "admin", "__password": self.password}
self.assertEqual(res.status_int, 303)
files = [("json", "loutre.json", json.dumps(queries).encode("utf-8"))]
{"csrf_token": csrf_token},
"Accept": "application/json",
def test_rewrite_args_errors(self):
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment