HTTP Origin is only sent on cross-origin requests in Firefox

Therefore, only raise Forbidden if Origin (or Referer for MSIE) is sent
(which is a protected header and all modern browsers (except IE)).

Also add a basic unit test which asserts the failure for false origins.
This commit is contained in:
Martin Zimmermann 2013-12-03 11:23:54 +01:00
parent 8802b73b52
commit b839b2be31
3 changed files with 30 additions and 12 deletions

View File

@ -33,8 +33,8 @@ class JSON(Response):
def csrf(view): def csrf(view):
"""A decorator to check if HTTP_Origin matches configured host. If not, """A decorator to check if Origin matches Host (may be empty if in the same
return 401 Forbidden. See origin, except for IE of course). When MSIE, use Referer. See
* https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet#Checking_The_Origin_Header * 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 * http://tools.ietf.org/html/draft-abarth-origin-09
@ -45,12 +45,14 @@ def csrf(view):
def dec(self, environ, request, *args, **kwargs): def dec(self, environ, request, *args, **kwargs):
hosts = map(parse.host, self.conf.getiter("host"))
if UserAgent(environ).browser == "msie": # yup if UserAgent(environ).browser == "msie": # yup
origin = request.headers.get("Referer", "") if parse.host(request.headers.get("Referer", "")) not in hosts:
else: raise Forbidden("CSRF")
origin = request.headers.get("Origin", "") elif "Origin" in request.headers:
if parse.host(origin) not in map(parse.host, self.conf.getiter("host")): if parse.host(request.headers.get("Origin", "")) not in hosts:
raise Forbidden("CSRF") raise Forbidden("CSRF")
return view(self, environ, request, *args, **kwargs) return view(self, environ, request, *args, **kwargs)

View File

@ -11,7 +11,6 @@ class FakeIP(object):
def __call__(self, environ, start_response): def __call__(self, environ, start_response):
environ['REMOTE_ADDR'] = self.ip environ['REMOTE_ADDR'] = self.ip
environ['HTTP_ORIGIN'] = "http://localhost:8080"
return self.app(environ, start_response) return self.app(environ, start_response)

View File

@ -269,10 +269,27 @@ class TestComments(unittest.TestCase):
def testDeleteCommentRemovesThread(self): def testDeleteCommentRemovesThread(self):
rv = self.client.post('/new?uri=%2F', data=json.dumps({"text": "..."})) rv = self.client.post('/new?uri=%2F', data=json.dumps({"text": "..."}))
assert '/' in self.app.db.threads assert '/' in self.app.db.threads
self.client.delete('/id/1') self.client.delete('/id/1')
assert '/' not in self.app.db.threads assert '/' not in self.app.db.threads
def testCSRF(self):
payload = json.dumps({"text": "..."})
assert self.client.post('/new?uri=%2F', data=payload,
headers={"Origin": "http://localhost:8080"}
).status_code == 201
assert self.client.post('/new?uri=%2F', data=payload,
headers={"Referer": "http://other.example/asdf",
"User-Agent": "msie"}
).status_code == 403
assert self.client.post('/new?uri=%2F', data=payload,
headers={"Origin": "http://other.example"}
).status_code == 403
class TestModeratedComments(unittest.TestCase): class TestModeratedComments(unittest.TestCase):