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):
"""A decorator to check if HTTP_Origin matches configured host. If not,
return 401 Forbidden. See
"""A decorator to check if Origin matches Host (may be empty if in the same
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
* http://tools.ietf.org/html/draft-abarth-origin-09
@ -45,12 +45,14 @@ def csrf(view):
def dec(self, environ, request, *args, **kwargs):
hosts = map(parse.host, self.conf.getiter("host"))
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")
if parse.host(request.headers.get("Referer", "")) not in hosts:
raise Forbidden("CSRF")
elif "Origin" in request.headers:
if parse.host(request.headers.get("Origin", "")) not in hosts:
raise Forbidden("CSRF")
return view(self, environ, request, *args, **kwargs)

View File

@ -11,7 +11,6 @@ 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)

View File

@ -269,10 +269,27 @@ class TestComments(unittest.TestCase):
def testDeleteCommentRemovesThread(self):
rv = self.client.post('/new?uri=%2F', data=json.dumps({"text": "..."}))
assert '/' in self.app.db.threads
self.client.delete('/id/1')
assert '/' not in self.app.db.threads
rv = self.client.post('/new?uri=%2F', data=json.dumps({"text": "..."}))
assert '/' in self.app.db.threads
self.client.delete('/id/1')
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):