# HG changeset patch # User Søren Løvborg # Date 1443017354 -7200 # Node ID 38d1c99cd0005c1df5a37692615356c918dbe068 # Parent 092ea4d40d605fd37b2c92c4cc354912049f4035 login: enhance came_from validation Drop urlparse and just validate that came_from is a RFC 3986 compliant path. This blocks an HTTP header injection vulnerability discovered by Gjoko Krstic of Zero Science Lab (CVE-2015-5285) diff -r 092ea4d40d60 -r 38d1c99cd000 kallithea/controllers/login.py --- a/kallithea/controllers/login.py Wed Sep 30 23:19:46 2015 +0200 +++ b/kallithea/controllers/login.py Wed Sep 23 16:09:14 2015 +0200 @@ -27,8 +27,8 @@ import logging +import re import formencode -import urlparse from formencode import htmlfill from webob.exc import HTTPFound, HTTPBadRequest @@ -56,10 +56,19 @@ def __before__(self): super(LoginController, self).__before__() - def _validate_came_from(self, came_from): - """Return True if came_from is valid and can and should be used""" - url = urlparse.urlsplit(came_from) - return not url.scheme and not url.netloc + def _validate_came_from(self, came_from, + _re=re.compile(r"/(?!/)[-!#$%&'()*+,./:;=?@_~0-9A-Za-z]*$")): + """Return True if came_from is valid and can and should be used. + + Determines if a URI reference is valid and relative to the origin; + or in RFC 3986 terms, whether it matches this production: + + origin-relative-ref = path-absolute [ "?" query ] [ "#" fragment ] + + with the exception that '%' escapes are not validated and '#' is + allowed inside the fragment part. + """ + return _re.match(came_from) is not None def index(self): c.came_from = safe_str(request.GET.get('came_from', '')) diff -r 092ea4d40d60 -r 38d1c99cd000 kallithea/tests/functional/test_login.py --- a/kallithea/tests/functional/test_login.py Wed Sep 30 23:19:46 2015 +0200 +++ b/kallithea/tests/functional/test_login.py Wed Sep 23 16:09:14 2015 +0200 @@ -107,6 +107,9 @@ ('ftp://ftp.example.com',), ('http://other.example.com/bl%C3%A5b%C3%A6rgr%C3%B8d',), ('//evil.example.com/',), + ('/\r\nX-Header-Injection: boo',), + ('/invälid_url_bytes',), + ('non-absolute-path',), ]) def test_login_bad_came_froms(self, url_came_from): response = self.app.post(url(controller='login', action='index',