From 32e4b70510103bb55e6aa0be15b118ea0b01342d Mon Sep 17 00:00:00 2001 From: Martin Zimmermann Date: Sun, 1 Dec 2013 13:24:37 +0100 Subject: [PATCH 1/3] check if Origin matches Host to mitigate CSRF, part of #40 --- isso/views/comments.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/isso/views/comments.py b/isso/views/comments.py index 0fe1a0a..90de4f6 100644 --- a/isso/views/comments.py +++ b/isso/views/comments.py @@ -31,6 +31,28 @@ class JSON(Response): return super(JSON, self).__init__(*args, content_type='application/json') +def csrf(view): + """A decorator to check if HTTP_Origin matches configured host. If not, + return 401 Forbidden. See + + * https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet#Checking_The_Origin_Header + * http://tools.ietf.org/html/draft-abarth-origin-09 + * https://wiki.mozilla.org/Security/Origin + + for details. + """ + + def dec(self, environ, request, *args, **kwargs): + + origin = request.headers.get("Origin") + if parse.host(origin) not in map(parse.host, self.conf.getiter("host")): + raise Forbidden("CSRF") + + return view(self, environ, request, *args, **kwargs) + + return dec + + class API(object): FIELDS = set(['id', 'parent', 'text', 'author', 'website', 'email', @@ -91,6 +113,7 @@ class API(object): return True, "" + @csrf @requires(str, 'uri') def new(self, environ, request, uri): @@ -174,6 +197,7 @@ class API(object): return Response(json.dumps(rv), 200, content_type='application/json') + @csrf def edit(self, environ, request, id): try: @@ -217,6 +241,7 @@ class API(object): resp.headers.add("X-Set-Cookie", cookie("isso-%i" % rv["id"])) return resp + @csrf def delete(self, environ, request, id, key=None): try: @@ -294,11 +319,13 @@ class API(object): return JSON(json.dumps(rv), 200) + @csrf def like(self, environ, request, id): nv = self.comments.vote(True, id, utils.anonymize(str(request.remote_addr))) return Response(json.dumps(nv), 200) + @csrf def dislike(self, environ, request, id): nv = self.comments.vote(False, id, utils.anonymize(str(request.remote_addr))) From 4c16ba76cca35e528efdbcf43a205271388fb4e4 Mon Sep 17 00:00:00 2001 From: Martin Zimmermann Date: Sun, 1 Dec 2013 13:38:41 +0100 Subject: [PATCH 2/3] fix unittests --- isso/views/comments.py | 2 +- specs/fixtures.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/isso/views/comments.py b/isso/views/comments.py index 90de4f6..24a907e 100644 --- a/isso/views/comments.py +++ b/isso/views/comments.py @@ -44,7 +44,7 @@ def csrf(view): def dec(self, environ, request, *args, **kwargs): - origin = request.headers.get("Origin") + origin = request.headers.get("Origin", "") if parse.host(origin) not in map(parse.host, self.conf.getiter("host")): raise Forbidden("CSRF") diff --git a/specs/fixtures.py b/specs/fixtures.py index 91f8349..d61c7fa 100644 --- a/specs/fixtures.py +++ b/specs/fixtures.py @@ -11,6 +11,7 @@ class FakeIP(object): def __call__(self, environ, start_response): environ['REMOTE_ADDR'] = self.ip + environ['HTTP_ORIGIN'] = "http://localhost:8080" return self.app(environ, start_response) From 9a03cca7939668357d739489788cca094b7df564 Mon Sep 17 00:00:00 2001 From: Martin Zimmermann Date: Mon, 2 Dec 2013 12:06:07 +0100 Subject: [PATCH 3/3] use Referer instead of Origin when using IE * IE10 (and 11) do not send HTTP_ORIGIN when requesting a URL no in the same origin, although recommended by WHATWG [1] * if IE10 is used, use the referer. If this header is supressed by the user, it won't work (and I don't care). IE10 needs to die, seriously: > We have a long-standing interoperability difference with other browsers > where we treat different ports as same-origin whereas other browsers > treat them as cross-origin. via https://connect.microsoft.com/IE/feedback/details/781303/origin-header-is-not-added-to-cors-requests-to-same-domain-but-different-port [1] http://tools.ietf.org/html/draft-abarth-origin-09 --- isso/views/comments.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/isso/views/comments.py b/isso/views/comments.py index 24a907e..1bec27b 100644 --- a/isso/views/comments.py +++ b/isso/views/comments.py @@ -12,6 +12,7 @@ from werkzeug.http import dump_cookie from werkzeug.routing import Rule from werkzeug.wrappers import Response from werkzeug.exceptions import BadRequest, Forbidden, NotFound +from werkzeug.useragents import UserAgent from isso.compat import text_type as str @@ -44,7 +45,10 @@ def csrf(view): def dec(self, environ, request, *args, **kwargs): - origin = request.headers.get("Origin", "") + if UserAgent(environ).browser == "msie": # yup + origin = request.headers.get("Referer", "") + else: + origin = request.headers.get("Origin", "") if parse.host(origin) not in map(parse.host, self.conf.getiter("host")): raise Forbidden("CSRF")