Skip to content

Commit 0c149b0

Browse files
Merge pull request #149 from django-commons/issue/135-crash-cookie-str-true
Make cookie parsing and serialization more robust
2 parents 603973d + bea9422 commit 0c149b0

File tree

4 files changed

+78
-10
lines changed

4 files changed

+78
-10
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ dist/
2222
htmlcov/
2323
reports/
2424
testapp/*.db
25+
.hypothesis
2526

2627
# frontend tooling / builds
2728
js/node_modules/

cookie_consent/util.py

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,64 @@
11
# -*- coding: utf-8 -*-
22
import datetime
3-
from typing import Union
3+
import logging
4+
from typing import Dict, Union
45

56
from .cache import all_cookie_groups, get_cookie, get_cookie_group
67
from .conf import settings
78
from .models import ACTION_ACCEPTED, ACTION_DECLINED, LogItem
89

10+
logger = logging.getLogger(__name__)
911

10-
def parse_cookie_str(cookie):
11-
dic = {}
12+
COOKIE_GROUP_SEP = "|"
13+
KEY_VALUE_SEP = "="
14+
15+
16+
def parse_cookie_str(cookie: str) -> Dict[str, str]:
1217
if not cookie:
13-
return dic
14-
for c in cookie.split("|"):
15-
key, value = c.split("=")
16-
dic[key] = value
17-
return dic
18+
return {}
19+
20+
bits = cookie.split(COOKIE_GROUP_SEP)
21+
22+
def _gen_pairs():
23+
for possible_pair in bits:
24+
parts = possible_pair.split(KEY_VALUE_SEP)
25+
if len(parts) == 2:
26+
yield parts
27+
else:
28+
logger.debug("cookie_value_discarded", extra={"value": possible_pair})
29+
30+
return dict(_gen_pairs())
31+
32+
33+
def _contains_invalid_characters(*inputs: str) -> bool:
34+
# = and | are special separators. They are unexpected characters in both
35+
# keys and values.
36+
for separator in (COOKIE_GROUP_SEP, KEY_VALUE_SEP):
37+
for value in inputs:
38+
if separator in value:
39+
logger.debug("skip_separator", extra={"value": value, "sep": separator})
40+
return True
41+
return False
42+
43+
44+
def dict_to_cookie_str(dic) -> str:
45+
"""
46+
Serialize a dictionary of cookie-group metadata to a string.
47+
48+
The result is stored in a cookie itself. Note that the dictionary keys are expected
49+
to be cookie group ``varname`` fields, which are validated against a slug regex. The
50+
values are supposed to be ISO-8601 timestamps.
51+
52+
Invalid key/value pairs are dropped.
53+
"""
1854

55+
def _gen_pairs():
56+
for key, value in dic.items():
57+
if _contains_invalid_characters(key, value):
58+
continue
59+
yield f"{key}={value}"
1960

20-
def dict_to_cookie_str(dic):
21-
return "|".join(["%s=%s" % (k, v) for k, v in dic.items() if v])
61+
return "|".join(_gen_pairs())
2262

2363

2464
def get_cookie_dict_from_request(request):
@@ -171,6 +211,7 @@ def is_cookie_consent_enabled(request):
171211
return enabled
172212

173213

214+
# Deprecated
174215
def get_cookie_string(cookie_dic):
175216
"""
176217
Returns cookie in format suitable for use in javascript.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ tests = [
4848
"pytest",
4949
"pytest-django",
5050
"pytest-playwright",
51+
"hypothesis",
5152
"tox",
5253
"isort",
5354
"black",

tests/test_util.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
# -*- coding: utf-8 -*-
22
from datetime import datetime
33

4+
from django.http import parse_cookie
45
from django.test import TestCase
56
from django.test.client import RequestFactory
67
from django.test.utils import override_settings
78

9+
from hypothesis import example, given, strategies as st
10+
811
from cookie_consent.conf import settings
912
from cookie_consent.models import Cookie, CookieGroup
1013
from cookie_consent.util import (
@@ -127,3 +130,25 @@ def test_get_accepted_cookies(self):
127130
self.request.COOKIES[settings.COOKIE_CONSENT_NAME] = cookie_str
128131
cookies = get_accepted_cookies(self.request)
129132
self.assertIn(self.cookie, cookies)
133+
134+
135+
@example({"": "|"})
136+
@example({"": "="})
137+
@given(
138+
cookie_dict=st.dictionaries(
139+
keys=st.text(min_size=0),
140+
values=st.text(min_size=0),
141+
)
142+
)
143+
def test_serialize_and_parse_cookie_str(cookie_dict):
144+
serialized = dict_to_cookie_str(cookie_dict)
145+
parsed = parse_cookie_str(serialized)
146+
147+
assert len(parsed.keys()) <= len(cookie_dict.keys())
148+
149+
150+
@given(cookie_str=st.text(min_size=0))
151+
def test_parse_cookie_str(cookie_str: str):
152+
parsed = parse_cookie_str(cookie_str)
153+
154+
assert isinstance(parsed, dict)

0 commit comments

Comments
 (0)