From 7994a9cc5d55b34c0c2495b126bbb40410c621a8 Mon Sep 17 00:00:00 2001 From: dataflow-solutions-sk Date: Mon, 17 Aug 2026 10:33:03 +0200 Subject: [PATCH] Unescape cookie values stored via set_cookie in the cookies property TestApp.set_cookie() escapes the value with escape_cookie_value() (wraps it in quotes and substitutes special characters), but the cookies property returned cookie.value verbatim, so cookies.get(name) returned the raw quoted/escaped value instead of the original one. Add unescape_cookie_value() in webtest/compat.py, the inverse of escape_cookie_value(), and apply it in TestApp.cookies. Values that aren't wrapped in double quotes (e.g. cookies already unquoted by the cookiejar when parsing a real Set-Cookie header) are returned unchanged, so this doesn't affect cookies set via HTTP responses. --- tests/test_app.py | 7 +++++++ webtest/app.py | 6 +++++- webtest/compat.py | 38 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/tests/test_app.py b/tests/test_app.py index 6ab109a..6314c4d 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -206,6 +206,13 @@ def cookie_app(environ, start_response): app.get('/') app.reset() + def test_set_cookie_value_is_unescaped_in_cookies_property(self): + app = webtest.TestApp(debug_app) + app.set_cookie('foo', 'bar') + app.set_cookie('fizz', ';bar=baz') # Make sure we're unescaping. + self.assertEqual(app.cookies.get('foo'), 'bar') + self.assertEqual(app.cookies.get('fizz'), ';bar=baz') + def test_preserves_cookies(self): def cookie_app(environ, start_response): req = Request(environ) diff --git a/webtest/app.py b/webtest/app.py index 9304288..8fac557 100644 --- a/webtest/app.py +++ b/webtest/app.py @@ -22,6 +22,7 @@ from webtest.compat import urlparse from webtest.compat import to_bytes from webtest.compat import escape_cookie_value +from webtest.compat import unescape_cookie_value from webtest.response import TestResponse from webtest import forms from webtest import lint @@ -228,7 +229,10 @@ def set_authorization(self, value): @property def cookies(self): - return {cookie.name: cookie.value for cookie in self.cookiejar} + return { + cookie.name: unescape_cookie_value(cookie.value) + for cookie in self.cookiejar + } def set_cookie(self, name, value): """ diff --git a/webtest/compat.py b/webtest/compat.py index aa6e26d..33ff8c8 100644 --- a/webtest/compat.py +++ b/webtest/compat.py @@ -1,3 +1,4 @@ +import re import sys from http import cookies @@ -31,6 +32,43 @@ def escape_cookie_value(value): ) + '"' +_OCTAL_ESCAPE_RE = re.compile(r'\\[0-3][0-7][0-7]') +_QUOTED_PAIR_RE = re.compile(r'\\.') + + +def unescape_cookie_value(value): + """ + Reverses the transformation performed by ``escape_cookie_value``. + + If the value isn't wrapped in double quotes it is returned unchanged, + since it can't have been produced by ``escape_cookie_value`` (this also + covers values that a cookiejar has already unquoted for us). + + """ + if value is None or len(value) < 2 or value[0] != '"' or value[-1] != '"': + return value + + value = value[1:-1] + + result = [] + i = 0 + length = len(value) + while i < length: + octal_match = _OCTAL_ESCAPE_RE.match(value, i) + if octal_match: + result.append(chr(int(value[i + 1:i + 4], 8))) + i += 4 + continue + quoted_pair_match = _QUOTED_PAIR_RE.match(value, i) + if quoted_pair_match: + result.append(value[i + 1]) + i += 2 + continue + result.append(value[i]) + i += 1 + return ''.join(result) + + # A list of illegal characters in a cookie and the escaped equivalent. # Taken from Python's cookie module. COOKIE_ESCAPE_CHAR_MAP = {