[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]
[tor-commits] [ooni-probe/master] Implement CSRF protection based on double-submit token.
commit 84e7083fe0c0ad53286756395f07b7a9cbb10c18
Author: Arturo Filastò <arturo@xxxxxxxxxxx>
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)
_______________________________________________
tor-commits mailing list
tor-commits@xxxxxxxxxxxxxxxxxxxx
https://lists.torproject.org/cgi-bin/mailman/listinfo/tor-commits