High severity7.5NVD Advisory· Published Oct 3, 2016· Updated May 6, 2026
CVE-2016-7401
CVE-2016-7401
Description
The cookie parsing code in Django before 1.8.15 and 1.9.x before 1.9.10, when used on a site with Google Analytics, allows remote attackers to bypass an intended CSRF protection mechanism by setting arbitrary cookies.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
DjangoPyPI | < 1.8.15 | 1.8.15 |
DjangoPyPI | >= 1.9, < 1.9.10 | 1.9.10 |
Patches
36fe846a8f08dAdded CVE-2016-7401 to the security release archive.
1 file changed · +12 −0
docs/releases/security.txt+12 −0 modified@@ -769,3 +769,15 @@ Versions affected * Django 1.9 `(patch) <https://github.com/django/django/commit/d03bf6fe4e9bf5b07de62c1a271c4b41a7d3d158>`__ * Django 1.8 `(patch) <https://github.com/django/django/commit/f68e5a99164867ab0e071a936470958ed867479d>`__ + +September 26, 2016 - :cve:`2016-7401` +------------------------------------- + +CSRF protection bypass on a site with Google Analytics. `Full description +<https://www.djangoproject.com/weblog/2016/sep/26/security-releases/>`__ + +Versions affected +~~~~~~~~~~~~~~~~~ + +* Django 1.9 `(patch) <https://github.com/django/django/commit/d1bc980db1c0fffd6d60677e62f70beadb9fe64a>`__ +* Django 1.8 `(patch) <https://github.com/django/django/commit/6118ab7d0676f0d622278e5be215f14fb5410b6a>`__
6118ab7d0676[1.8.x] Fixed CVE-2016-7401 -- Fixed CSRF protection bypass on a site with Google Analytics.
5 files changed · +87 −18
django/http/cookie.py+16 −13 modified@@ -89,18 +89,21 @@ def _BaseCookie__set(self, key, real_value, coded_value): def parse_cookie(cookie): - if cookie == '': - return {} - if not isinstance(cookie, http_cookies.BaseCookie): - try: - c = SimpleCookie() - c.load(cookie) - except http_cookies.CookieError: - # Invalid cookie - return {} - else: - c = cookie + """ + Return a dictionary parsed from a `Cookie:` header string. + """ cookiedict = {} - for key in c.keys(): - cookiedict[key] = c.get(key).value + if six.PY2: + cookie = force_str(cookie) + for chunk in cookie.split(str(';')): + if str('=') in chunk: + key, val = chunk.split(str('='), 1) + else: + # Assume an empty name per + # https://bugzilla.mozilla.org/show_bug.cgi?id=169091 + key, val = str(''), chunk + key, val = key.strip(), val.strip() + if key or val: + # unquote using Python's algorithm. + cookiedict[key] = http_cookies._unquote(val) return cookiedict
docs/releases/1.8.15.txt+18 −0 added@@ -0,0 +1,18 @@ +=========================== +Django 1.8.15 release notes +=========================== + +*September 26, 2016* + +Django 1.8.15 fixes a security issue in 1.8.14. + +CSRF protection bypass on a site with Google Analytics +====================================================== + +An interaction between Google Analytics and Django's cookie parsing could allow +an attacker to set arbitrary cookies leading to a bypass of CSRF protection. + +The parser for ``request.COOKIES`` is simplified to better match the behavior +of browsers and to mitigate this attack. ``request.COOKIES`` may now contain +cookies that are invalid according to :rfc:`6265` but are possible to set via +``document.cookie``.
docs/releases/index.txt+1 −0 modified@@ -25,6 +25,7 @@ versions of the documentation contain the release notes for any later releases. .. toctree:: :maxdepth: 1 + 1.8.15 1.8.14 1.8.13 1.8.12
tests/httpwrappers/tests.py+51 −1 modified@@ -21,7 +21,7 @@ from django.test import TestCase from django.utils import six from django.utils._os import upath -from django.utils.encoding import force_text, smart_str +from django.utils.encoding import force_str, force_text, smart_str from django.utils.functional import lazy lazystr = lazy(force_text, six.text_type) @@ -643,6 +643,8 @@ def test_decode(self): c2 = SimpleCookie() c2.load(c.output()[12:]) self.assertEqual(c['test'].value, c2['test'].value) + c3 = parse_cookie(c.output()[12:]) + self.assertEqual(c['test'].value, c3['test']) def test_decode_2(self): """ @@ -653,6 +655,8 @@ def test_decode_2(self): c2 = SimpleCookie() c2.load(c.output()[12:]) self.assertEqual(c['test'].value, c2['test'].value) + c3 = parse_cookie(c.output()[12:]) + self.assertEqual(c['test'].value, c3['test']) def test_nonstandard_keys(self): """ @@ -666,6 +670,52 @@ def test_repeated_nonstandard_keys(self): """ self.assertIn('good_cookie', parse_cookie('a:=b; a:=c; good_cookie=yes').keys()) + def test_python_cookies(self): + """ + Test cases copied from Python's Lib/test/test_http_cookies.py + """ + self.assertEqual(parse_cookie('chips=ahoy; vienna=finger'), {'chips': 'ahoy', 'vienna': 'finger'}) + # Here parse_cookie() differs from Python's cookie parsing in that it + # treats all semicolons as delimiters, even within quotes. + self.assertEqual( + parse_cookie('keebler="E=mc2; L=\\"Loves\\"; fudge=\\012;"'), + {'keebler': '"E=mc2', 'L': '\\"Loves\\"', 'fudge': '\\012', '': '"'} + ) + # Illegal cookies that have an '=' char in an unquoted value. + self.assertEqual(parse_cookie('keebler=E=mc2'), {'keebler': 'E=mc2'}) + # Cookies with ':' character in their name. + self.assertEqual(parse_cookie('key:term=value:term'), {'key:term': 'value:term'}) + # Cookies with '[' and ']'. + self.assertEqual(parse_cookie('a=b; c=[; d=r; f=h'), {'a': 'b', 'c': '[', 'd': 'r', 'f': 'h'}) + + def test_cookie_edgecases(self): + # Cookies that RFC6265 allows. + self.assertEqual(parse_cookie('a=b; Domain=example.com'), {'a': 'b', 'Domain': 'example.com'}) + # parse_cookie() has historically kept only the last cookie with the + # same name. + self.assertEqual(parse_cookie('a=b; h=i; a=c'), {'a': 'c', 'h': 'i'}) + + def test_invalid_cookies(self): + """ + Cookie strings that go against RFC6265 but browsers will send if set + via document.cookie. + """ + # Chunks without an equals sign appear as unnamed values per + # https://bugzilla.mozilla.org/show_bug.cgi?id=169091 + self.assertIn('django_language', parse_cookie('abc=def; unnamed; django_language=en').keys()) + # Even a double quote may be an unamed value. + self.assertEqual(parse_cookie('a=b; "; c=d'), {'a': 'b', '': '"', 'c': 'd'}) + # Spaces in names and values, and an equals sign in values. + self.assertEqual(parse_cookie('a b c=d e = f; gh=i'), {'a b c': 'd e = f', 'gh': 'i'}) + # More characters the spec forbids. + self.assertEqual(parse_cookie('a b,c<>@:/[]?{}=d " =e,f g'), {'a b,c<>@:/[]?{}': 'd " =e,f g'}) + # Unicode characters. The spec only allows ASCII. + self.assertEqual(parse_cookie('saint=André Bessette'), {'saint': force_str('André Bessette')}) + # Browsers don't send extra whitespace or semicolons in Cookie headers, + # but parse_cookie() should parse whitespace the same way + # document.cookie parses whitespace. + self.assertEqual(parse_cookie(' = b ; ; = ; c = ; '), {'': 'b', 'c': ''}) + def test_httponly_after_load(self): """ Test that we can use httponly attribute on cookies that we load
tests/requests/tests.py+1 −4 modified@@ -10,7 +10,7 @@ from django.core.handlers.wsgi import LimitedStream, WSGIRequest from django.http import ( HttpRequest, HttpResponse, RawPostDataException, UnreadablePostError, - build_request_repr, parse_cookie, + build_request_repr, ) from django.test import RequestFactory, SimpleTestCase, override_settings from django.test.client import FakePayload @@ -161,9 +161,6 @@ def wsgi_str(path_info): request = WSGIRequest({'PATH_INFO': wsgi_str("/سلام/"), 'REQUEST_METHOD': 'get', 'wsgi.input': BytesIO(b'')}) self.assertEqual(request.path, "/سلام/") - def test_parse_cookie(self): - self.assertEqual(parse_cookie('invalid@key=true'), {}) - def test_httprequest_location(self): request = HttpRequest() self.assertEqual(request.build_absolute_uri(location="https://www.example.com/asdf"),
d1bc980db1c0[1.9.x] Fixed CVE-2016-7401 -- Fixed CSRF protection bypass on a site with Google Analytics.
6 files changed · +105 −18
django/http/cookie.py+16 −13 modified@@ -89,18 +89,21 @@ def _BaseCookie__set(self, key, real_value, coded_value): def parse_cookie(cookie): - if cookie == '': - return {} - if not isinstance(cookie, http_cookies.BaseCookie): - try: - c = SimpleCookie() - c.load(cookie) - except http_cookies.CookieError: - # Invalid cookie - return {} - else: - c = cookie + """ + Return a dictionary parsed from a `Cookie:` header string. + """ cookiedict = {} - for key in c.keys(): - cookiedict[key] = c.get(key).value + if six.PY2: + cookie = force_str(cookie) + for chunk in cookie.split(str(';')): + if str('=') in chunk: + key, val = chunk.split(str('='), 1) + else: + # Assume an empty name per + # https://bugzilla.mozilla.org/show_bug.cgi?id=169091 + key, val = str(''), chunk + key, val = key.strip(), val.strip() + if key or val: + # unquote using Python's algorithm. + cookiedict[key] = http_cookies._unquote(val) return cookiedict
docs/releases/1.8.15.txt+18 −0 added@@ -0,0 +1,18 @@ +=========================== +Django 1.8.15 release notes +=========================== + +*September 26, 2016* + +Django 1.8.15 fixes a security issue in 1.8.14. + +CSRF protection bypass on a site with Google Analytics +====================================================== + +An interaction between Google Analytics and Django's cookie parsing could allow +an attacker to set arbitrary cookies leading to a bypass of CSRF protection. + +The parser for ``request.COOKIES`` is simplified to better match the behavior +of browsers and to mitigate this attack. ``request.COOKIES`` may now contain +cookies that are invalid according to :rfc:`6265` but are possible to set via +``document.cookie``.
docs/releases/1.9.10.txt+18 −0 added@@ -0,0 +1,18 @@ +=========================== +Django 1.9.10 release notes +=========================== + +*September 26, 2016* + +Django 1.9.10 fixes a security issue in 1.9.9. + +CSRF protection bypass on a site with Google Analytics +====================================================== + +An interaction between Google Analytics and Django's cookie parsing could allow +an attacker to set arbitrary cookies leading to a bypass of CSRF protection. + +The parser for ``request.COOKIES`` is simplified to better match the behavior +of browsers and to mitigate this attack. ``request.COOKIES`` may now contain +cookies that are invalid according to :rfc:`6265` but are possible to set via +``document.cookie``.
docs/releases/index.txt+2 −0 modified@@ -25,6 +25,7 @@ versions of the documentation contain the release notes for any later releases. .. toctree:: :maxdepth: 1 + 1.9.10 1.9.9 1.9.8 1.9.7 @@ -41,6 +42,7 @@ versions of the documentation contain the release notes for any later releases. .. toctree:: :maxdepth: 1 + 1.8.15 1.8.14 1.8.13 1.8.12
tests/httpwrappers/tests.py+51 −1 modified@@ -21,7 +21,7 @@ from django.test import SimpleTestCase from django.utils import six from django.utils._os import upath -from django.utils.encoding import force_text, smart_str +from django.utils.encoding import force_str, force_text, smart_str from django.utils.functional import lazy lazystr = lazy(force_text, six.text_type) @@ -657,6 +657,8 @@ def test_decode(self): c2 = SimpleCookie() c2.load(c.output()[12:]) self.assertEqual(c['test'].value, c2['test'].value) + c3 = parse_cookie(c.output()[12:]) + self.assertEqual(c['test'].value, c3['test']) def test_decode_2(self): """ @@ -667,6 +669,8 @@ def test_decode_2(self): c2 = SimpleCookie() c2.load(c.output()[12:]) self.assertEqual(c['test'].value, c2['test'].value) + c3 = parse_cookie(c.output()[12:]) + self.assertEqual(c['test'].value, c3['test']) def test_nonstandard_keys(self): """ @@ -680,6 +684,52 @@ def test_repeated_nonstandard_keys(self): """ self.assertIn('good_cookie', parse_cookie('a:=b; a:=c; good_cookie=yes').keys()) + def test_python_cookies(self): + """ + Test cases copied from Python's Lib/test/test_http_cookies.py + """ + self.assertEqual(parse_cookie('chips=ahoy; vienna=finger'), {'chips': 'ahoy', 'vienna': 'finger'}) + # Here parse_cookie() differs from Python's cookie parsing in that it + # treats all semicolons as delimiters, even within quotes. + self.assertEqual( + parse_cookie('keebler="E=mc2; L=\\"Loves\\"; fudge=\\012;"'), + {'keebler': '"E=mc2', 'L': '\\"Loves\\"', 'fudge': '\\012', '': '"'} + ) + # Illegal cookies that have an '=' char in an unquoted value. + self.assertEqual(parse_cookie('keebler=E=mc2'), {'keebler': 'E=mc2'}) + # Cookies with ':' character in their name. + self.assertEqual(parse_cookie('key:term=value:term'), {'key:term': 'value:term'}) + # Cookies with '[' and ']'. + self.assertEqual(parse_cookie('a=b; c=[; d=r; f=h'), {'a': 'b', 'c': '[', 'd': 'r', 'f': 'h'}) + + def test_cookie_edgecases(self): + # Cookies that RFC6265 allows. + self.assertEqual(parse_cookie('a=b; Domain=example.com'), {'a': 'b', 'Domain': 'example.com'}) + # parse_cookie() has historically kept only the last cookie with the + # same name. + self.assertEqual(parse_cookie('a=b; h=i; a=c'), {'a': 'c', 'h': 'i'}) + + def test_invalid_cookies(self): + """ + Cookie strings that go against RFC6265 but browsers will send if set + via document.cookie. + """ + # Chunks without an equals sign appear as unnamed values per + # https://bugzilla.mozilla.org/show_bug.cgi?id=169091 + self.assertIn('django_language', parse_cookie('abc=def; unnamed; django_language=en').keys()) + # Even a double quote may be an unamed value. + self.assertEqual(parse_cookie('a=b; "; c=d'), {'a': 'b', '': '"', 'c': 'd'}) + # Spaces in names and values, and an equals sign in values. + self.assertEqual(parse_cookie('a b c=d e = f; gh=i'), {'a b c': 'd e = f', 'gh': 'i'}) + # More characters the spec forbids. + self.assertEqual(parse_cookie('a b,c<>@:/[]?{}=d " =e,f g'), {'a b,c<>@:/[]?{}': 'd " =e,f g'}) + # Unicode characters. The spec only allows ASCII. + self.assertEqual(parse_cookie('saint=André Bessette'), {'saint': force_str('André Bessette')}) + # Browsers don't send extra whitespace or semicolons in Cookie headers, + # but parse_cookie() should parse whitespace the same way + # document.cookie parses whitespace. + self.assertEqual(parse_cookie(' = b ; ; = ; c = ; '), {'': 'b', 'c': ''}) + def test_httponly_after_load(self): """ Test that we can use httponly attribute on cookies that we load
tests/requests/tests.py+0 −4 modified@@ -10,7 +10,6 @@ from django.core.handlers.wsgi import LimitedStream, WSGIRequest from django.http import ( HttpRequest, HttpResponse, RawPostDataException, UnreadablePostError, - parse_cookie, ) from django.test import RequestFactory, SimpleTestCase, override_settings from django.test.client import FakePayload @@ -154,9 +153,6 @@ def wsgi_str(path_info): request = WSGIRequest({'PATH_INFO': wsgi_str("/سلام/"), 'REQUEST_METHOD': 'get', 'wsgi.input': BytesIO(b'')}) self.assertEqual(request.path, "/سلام/") - def test_parse_cookie(self): - self.assertEqual(parse_cookie('invalid@key=true'), {}) - def test_httprequest_location(self): request = HttpRequest() self.assertEqual(request.build_absolute_uri(location="https://www.example.com/asdf"),
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
20- www.djangoproject.com/weblog/2016/sep/26/security-releases/nvdPatchVendor Advisory
- www.debian.org/security/2016/dsa-3678nvdThird Party AdvisoryWEB
- www.securityfocus.com/bid/93182nvdThird Party Advisory
- www.securitytracker.com/id/1036899nvdThird Party Advisory
- www.ubuntu.com/usn/USN-3089-1nvdThird Party AdvisoryWEB
- github.com/advisories/GHSA-crhm-qpjc-cm64ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2016-7401ghsaADVISORY
- rhn.redhat.com/errata/RHSA-2016-2038.htmlnvdWEB
- rhn.redhat.com/errata/RHSA-2016-2039.htmlnvdWEB
- rhn.redhat.com/errata/RHSA-2016-2040.htmlnvdWEB
- rhn.redhat.com/errata/RHSA-2016-2041.htmlnvdWEB
- rhn.redhat.com/errata/RHSA-2016-2042.htmlnvdWEB
- rhn.redhat.com/errata/RHSA-2016-2043.htmlnvdWEB
- github.com/django/django/commit/6118ab7d0676f0d622278e5be215f14fb5410b6aghsaWEB
- github.com/django/django/commit/6fe846a8f08dc959003f298b5407e321c6fe3735ghsaWEB
- github.com/django/django/commit/d1bc980db1c0fffd6d60677e62f70beadb9fe64aghsaWEB
- github.com/pypa/advisory-database/tree/main/vulns/django/PYSEC-2016-3.yamlghsaWEB
- web.archive.org/web/20200227223637/http://www.securityfocus.com/bid/93182ghsaWEB
- web.archive.org/web/20210927195154/http://www.securitytracker.com/id/1036899ghsaWEB
- www.djangoproject.com/weblog/2016/sep/26/security-releasesghsaWEB
News mentions
0No linked articles in our index yet.