1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154
|
From f29171ded2147ba6935566741b1e993c7e848595 Mon Sep 17 00:00:00 2001
From: Collin Anderson <cmawebsite@gmail.com>
Date: Fri, 11 Mar 2016 21:36:08 -0500
Subject: [1.8.x] Fixed CVE-2016-7401 -- Fixed CSRF protection bypass on a site
with Google Analytics.
This is a security fix.
Backport of "refs #26158 -- rewrote http.parse_cookie() to better match
browsers." 93a135d111c2569d88d65a3f4ad9e6d9ad291452 from master
---
django/http/cookie.py | 29 ++++++++++++++------------
tests/httpwrappers/tests.py | 50 ++++++++++++++++++++++++++++++++++++++++++++-
tests/requests/tests.py | 5 +----
3 files changed, 66 insertions(+), 18 deletions(-)
diff --git a/django/http/cookie.py b/django/http/cookie.py
index 7084c87..07d8cbf 100644
--- a/django/http/cookie.py
+++ b/django/http/cookie.py
@@ -71,18 +71,21 @@ else:
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
diff --git a/tests/httpwrappers/tests.py b/tests/httpwrappers/tests.py
index 7881f2f..25a9127 100644
--- a/tests/httpwrappers/tests.py
+++ b/tests/httpwrappers/tests.py
@@ -19,7 +19,7 @@ from django.http import (QueryDict, HttpResponse, HttpResponseRedirect,
parse_cookie)
from django.test import TestCase
from django.utils.deprecation import RemovedInDjango18Warning
-from django.utils.encoding import smart_str, force_text
+from django.utils.encoding import smart_str, force_text, force_str
from django.utils.functional import lazy
from django.utils._os import upath
from django.utils import six
@@ -632,6 +632,8 @@ class CookieTests(unittest.TestCase):
c2 = SimpleCookie()
c2.load(c.output())
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):
"""
@@ -645,6 +647,52 @@ class CookieTests(unittest.TestCase):
"""
self.assertTrue('good_cookie' in 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
diff --git a/tests/requests/tests.py b/tests/requests/tests.py
index 55b37bb..6364ab1 100644
--- a/tests/requests/tests.py
+++ b/tests/requests/tests.py
@@ -11,7 +11,7 @@ from django.db import connection, connections
from django.core import signals
from django.core.exceptions import SuspiciousOperation
from django.core.handlers.wsgi import WSGIRequest, LimitedStream
-from django.http import (HttpRequest, HttpResponse, parse_cookie,
+from django.http import (HttpRequest, HttpResponse,
build_request_repr, UnreadablePostError, RawPostDataException)
from django.test import SimpleTestCase, TransactionTestCase, override_settings
from django.test.client import FakePayload
@@ -128,9 +128,6 @@ class RequestsTests(SimpleTestCase):
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"),
|