changeset 956:dabecab10faa

server (pages): impleted CSRF protection: A basic CSRF protection has been implemented using CSRF token. The token is created on session creation, and checked on data post. The process should be fully automatic, and a hidden field is added in forms in sat_templates when csrf_token is present in template data (require to import input/form.html with context). If token is wrong on absent, an unauthorized error page is returned (and client ip is logged). Also don't use anymore inlineCallbacks in _on_data_post, as StopIteration exception are catched by inlineCallbacks, resulting in bad behaviour. As a further security, getPostedDate raise a KeyError instead of StopIteration is a specific key is looked for and missing. Added HTTP_SEE_OTHER status code in constants.
author Goffi <goffi@goffi.org>
date Mon, 10 Jul 2017 19:10:31 +0200
parents 4f7cb6335a33
children 67bf14c91d5c
files src/server/constants.py src/server/server.py src/server/session_iface.py
diffstat 3 files changed, 31 insertions(+), 6 deletions(-) [+]
line wrap: on
line diff
--- a/src/server/constants.py	Sun Jul 09 22:28:40 2017 +0200
+++ b/src/server/constants.py	Mon Jul 10 19:10:31 2017 +0200
@@ -63,6 +63,7 @@
     HTTP_METHOD_POST = u'POST'
 
     ## HTTP codes ##
+    HTTP_SEE_OTHER = 303
     HTTP_BAD_REQUEST = 400
     HTTP_UNAUTHORIZED = 401
     HTTP_NOT_FOUND = 404
--- a/src/server/server.py	Sun Jul 09 22:28:40 2017 +0200
+++ b/src/server/server.py	Mon Jul 10 19:10:31 2017 +0200
@@ -1690,14 +1690,32 @@
             msg = failure_))
         self.pageError(request, C.HTTP_INTERNAL_ERROR)
 
-    @defer.inlineCallbacks
-    def _on_data_post(self, dummy, request):
-        yield defer.maybeDeferred(self.on_data_post, self, request)
-        request.setResponseCode(303)
+    def _on_data_post_redirect(self, ret, request):
+        """called when page's on_data_post has been called successfuly
+
+        this method redirect to the same page, using Post/Redirect/Get pattern
+        HTTP status code "See Other" (303) is the recommanded code in this case
+        """
+        request.setResponseCode(C.HTTP_SEE_OTHER)
         request.setHeader("location", request.uri)
         request.finish()
         raise failure.Failure(exceptions.CancelError(u'Post/Redirect/Get is used'))
 
+    def _on_data_post(self, dummy, request):
+        csrf_token = self.host.getSessionData(request, session_iface.ISATSession).csrf_token
+        try:
+            given_csrf = self.getPostedData(request, u'csrf_token')
+        except KeyError:
+            given_csrf = None
+        if given_csrf is None or given_csrf != csrf_token:
+            log.warning(_(u"invalid CSRF token, hack attempt? URL: {url}, IP: {ip}").format(
+                url=request.uri,
+                ip=request.getClientIP()))
+            self.pageError(request, C.HTTP_UNAUTHORIZED)
+        d = defer.maybeDeferred(self.on_data_post, self, request)
+        d.addCallback(self._on_data_post_redirect, request)
+        return d
+
 
     def getPostedData(self, request, keys, multiple=False):
         """get data from a POST request and decode it
@@ -1709,6 +1727,7 @@
         @param multiple(bool): True if multiple values are possible/expected
             if False, the first value is returned
         @return (iterator[unicode], list[iterator[unicode], unicode, list[unicode]): values received for this(these) key(s)
+        @raise KeyError: one specific key has been requested, and it is missing
         """
         if isinstance(keys, basestring):
             keys = [keys]
@@ -1722,7 +1741,10 @@
             if multiple:
                 ret.append(gen)
             else:
-                ret.append(next(gen))
+                try:
+                    ret.append(next(gen))
+                except StopIteration:
+                    return KeyError(key)
 
         return ret[0] if get_first else ret
 
@@ -1768,7 +1790,8 @@
         """Main method to handle the workflow of a LiberviaPage"""
         # template_data are the variables passed to template
         if not hasattr(request, 'template_data'):
-            request.template_data = {}
+            csrf_token = self.host.getSessionData(request, session_iface.ISATSession).csrf_token
+            request.template_data = {'csrf_token': csrf_token}
 
             # XXX: here is the code which need to be executed once
             #      at the beginning of the request hanling
--- a/src/server/session_iface.py	Sun Jul 09 22:28:40 2017 +0200
+++ b/src/server/session_iface.py	Mon Jul 10 19:10:31 2017 +0200
@@ -35,6 +35,7 @@
         self.jid = None
         self.uuid = unicode(shortuuid.uuid())
         self.identities = data_objects.Identities()
+        self.csrf_token = unicode(shortuuid.uuid())
 
 
 class ISATGuestSession(Interface):