[tor-commits] [ooni-probe/master] Implement CSRF protection based on double-submit token.

art at torproject.org art at torproject.org
Mon Sep 19 12:14:24 UTC 2016


commit 84e7083fe0c0ad53286756395f07b7a9cbb10c18
Author: Arturo Filastò <arturo at filasto.net>
Date:   Wed Jul 27 18:00:34 2016 +0200

    Implement CSRF protection based on double-submit token.
---
 ooni/deck.py          |  2 +-
 ooni/ui/web/server.py | 68 +++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 69 insertions(+), 1 deletion(-)

diff --git a/ooni/deck.py b/ooni/deck.py
index 7976e5e..6844fda 100644
--- a/ooni/deck.py
+++ b/ooni/deck.py
@@ -492,7 +492,7 @@ class InputStore(object):
     def list(self):
         if self._cache_stale:
             self._update_cache()
-        return self._cache
+        return deepcopy(self._cache)
 
     def get(self, input_id):
         if self._cache_stale:
diff --git a/ooni/ui/web/server.py b/ooni/ui/web/server.py
index f14f6b8..a6fd315 100644
--- a/ooni/ui/web/server.py
+++ b/ooni/ui/web/server.py
@@ -2,6 +2,9 @@ from __future__ import print_function
 
 import os
 import json
+import string
+from functools import wraps
+from random import SystemRandom
 
 from twisted.internet import defer, task, reactor
 from twisted.python import usage
@@ -26,11 +29,52 @@ def rpath(*path):
     context = os.path.abspath(os.path.dirname(__file__))
     return os.path.join(context, *path)
 
+
 class WebUIError(Exception):
     def __init__(self, code, message):
         self.code = code
         self.message = message
 
+
+def xsrf_protect(check=True):
+    """
+    This is a decorator that implements double submit token CSRF protection.
+
+    Basically we set a cookie and ensure that every request contains the
+    same value inside of the cookie and the request header.
+
+    It's based on the assumption that an attacker cannot read the cookie
+    that is set by the server (since it would be violating the SOP) and hence
+    is not possible to make a browser trigger requests that contain the
+    cookie value inside of the requests it sends.
+
+    If you wish to disable checking of the token set the value check to False.
+    This will still lead to the cookie being set.
+
+    This decorator needs to be applied after the decorator that registers
+    the routes.
+    """
+    def deco(f):
+
+        @wraps(f)
+        def wrapper(instance, request, *a, **kw):
+            should_check = check and instance._enable_xsrf_protection
+            token_cookie = request.getCookie(u'XSRF-TOKEN')
+            token_header = request.getHeader(b"X-XSRF-TOKEN")
+            if (token_cookie != instance._xsrf_token and
+                    instance._enable_xsrf_protection):
+                request.addCookie(u'XSRF-TOKEN',
+                                  instance._xsrf_token)
+            if should_check and token_cookie != token_header:
+                raise WebUIError(404, "Invalid XSRF token")
+            return f(instance, request, *a, **kw)
+
+        return wrapper
+
+    return deco
+
+
+
 class LongPoller(object):
     def __init__(self, timeout, _reactor=reactor):
         self.lock = defer.DeferredLock()
@@ -73,6 +117,7 @@ class WebUIAPI(object):
     # change happenned.
     _long_polling_timeout = 5
     _reactor = reactor
+    _enable_xsrf_protection = True
 
     def __init__(self, config, director, _reactor=reactor):
         self.director = director
@@ -80,6 +125,12 @@ class WebUIAPI(object):
         self.measurement_path = FilePath(config.measurements_directory)
         self.decks_path = FilePath(config.decks_directory)
 
+        # We use a double submit token to protect against XSRF
+        rng = SystemRandom()
+        token_space = string.letters+string.digits
+        self._xsrf_token = ''.join([rng.choice(token_space)
+                                    for _ in range(30)])
+
         self.status = {
             "software_version": ooniprobe_version,
             "software_name": "ooniprobe",
@@ -113,10 +164,12 @@ class WebUIAPI(object):
         self.status["country_code"] = probe_ip.geodata['countrycode']
 
     @app.handle_errors(NotFound)
+    @xsrf_protect(check=False)
     def not_found(self, request, _):
         request.redirect('/client/')
 
     @app.handle_errors(WebUIError)
+    @xsrf_protect(check=False)
     def web_ui_error(self, request, failure):
         error = failure.value
         request.setResponseCode(error.code)
@@ -132,6 +185,7 @@ class WebUIAPI(object):
         return json_string
 
     @app.route('/api/notify', methods=["GET"])
+    @xsrf_protect(check=False)
     def api_notify(self, request):
         def got_director_event(event):
             return self.render_json({
@@ -143,10 +197,12 @@ class WebUIAPI(object):
         return d
 
     @app.route('/api/status', methods=["GET"])
+    @xsrf_protect(check=False)
     def api_status(self, request):
         return self.render_json(self.status, request)
 
     @app.route('/api/status/update', methods=["GET"])
+    @xsrf_protect(check=False)
     def api_status_update(self, request):
         def got_status_update(event):
             return self.api_status(request)
@@ -155,14 +211,17 @@ class WebUIAPI(object):
         return d
 
     @app.route('/api/deck/generate', methods=["GET"])
+    @xsrf_protect(check=False)
     def api_deck_generate(self, request):
         return self.render_json({"generate": "deck"}, request)
 
     @app.route('/api/deck/<string:deck_name>/start', methods=["POST"])
+    @xsrf_protect(check=True)
     def api_deck_start(self, request, deck_name):
         return self.render_json({"start": deck_name}, request)
 
     @app.route('/api/deck', methods=["GET"])
+    @xsrf_protect(check=False)
     def api_deck_list(self, request):
         for deck_id in self.decks_path.listdir():
             pass
@@ -180,6 +239,7 @@ class WebUIAPI(object):
                                                             "Failed to start deck"))
 
     @app.route('/api/nettest/<string:test_name>/start', methods=["POST"])
+    @xsrf_protect(check=True)
     def api_nettest_start(self, request, test_name):
         try:
             _ = self.director.netTests[test_name]
@@ -220,10 +280,12 @@ class WebUIAPI(object):
         return self.render_json({"status": "started"}, request)
 
     @app.route('/api/nettest', methods=["GET"])
+    @xsrf_protect(check=False)
     def api_nettest_list(self, request):
         return self.render_json(self.director.netTests, request)
 
     @app.route('/api/input', methods=["GET"])
+    @xsrf_protect(check=False)
     def api_input_list(self, request):
         input_store_list = self.director.input_store.list()
         for key, value in input_store_list.items():
@@ -231,6 +293,7 @@ class WebUIAPI(object):
         return self.render_json(input_store_list, request)
 
     @app.route('/api/input/<string:input_id>/content', methods=["GET"])
+    @xsrf_protect(check=False)
     def api_input_content(self, request, input_id):
         content = self.director.input_store.getContent(input_id)
         request.setHeader('Content-Type', 'text/plain')
@@ -238,12 +301,14 @@ class WebUIAPI(object):
         return content
 
     @app.route('/api/input/<string:input_id>', methods=["GET"])
+    @xsrf_protect(check=False)
     def api_input_details(self, request, input_id):
         return self.render_json(
             self.director.input_store.get(input_id), request
         )
 
     @app.route('/api/measurement', methods=["GET"])
+    @xsrf_protect(check=False)
     def api_measurement_list(self, request):
         measurements = []
         for measurement_id in self.measurement_path.listdir():
@@ -264,6 +329,7 @@ class WebUIAPI(object):
         return self.render_json({"measurements": measurements}, request)
 
     @app.route('/api/measurement/<string:measurement_id>', methods=["GET"])
+    @xsrf_protect(check=False)
     def api_measurement_summary(self, request, measurement_id):
         try:
             measurement_dir = self.measurement_path.child(measurement_id)
@@ -289,6 +355,7 @@ class WebUIAPI(object):
 
     @app.route('/api/measurement/<string:measurement_id>/<int:idx>',
                methods=["GET"])
+    @xsrf_protect(check=False)
     def api_measurement_view(self, request, measurement_id, idx):
         try:
             measurement_dir = self.measurement_path.child(measurement_id)
@@ -310,6 +377,7 @@ class WebUIAPI(object):
         return self.render_json(r, request)
 
     @app.route('/client/', branch=True)
+    @xsrf_protect(check=False)
     def static(self, request):
         path = rpath("client")
         return static.File(path)





More information about the tor-commits mailing list