From 3737044efc5f5afa53f863831f57d27093d70ff9 Mon Sep 17 00:00:00 2001 From: David Lord Date: Mon, 4 Dec 2017 14:59:51 -0800 Subject: [PATCH] match header option value with single quotes `*=` only matches when followed by encoding comment regex using verbose mode and group names fixes #1091, fixes #1177 --- CHANGES.rst | 2 ++ tests/test_http.py | 8 ++++++++ werkzeug/http.py | 30 ++++++++++++++++++++++++------ 3 files changed, 34 insertions(+), 6 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 818a7dbb5..3cd9651ca 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -31,6 +31,7 @@ unreleased - Secure cookie contrib works with string secret key on Python 3. (`#1205`_) - Shared data middleware accepts a list instead of a dict of static locations to preserve lookup order. (`#1197`_) +- HTTP header values without encoding can contain single quotes. (`#1208`_) - The built-in dev server supports receiving requests with chunked transfer encoding. (`#1198`_) @@ -47,6 +48,7 @@ unreleased .. _#1197: https://github.com/pallets/werkzeug/pull/1197 .. _#1198: https://github.com/pallets/werkzeug/pull/1198 .. _#1205: https://github.com/pallets/werkzeug/pull/1205 +.. _#1208: https://github.com/pallets/werkzeug/pull/1208 Version 0.12.2 diff --git a/tests/test_http.py b/tests/test_http.py index 1fe992959..04ad73c28 100644 --- a/tests/test_http.py +++ b/tests/test_http.py @@ -280,6 +280,14 @@ def test_parse_options_header(self): ('form-data', {'name': u'\u016an\u012dc\u014dde\u033d', 'filename': 'some_file.txt'}) + def test_parse_options_header_value_with_quotes(self): + assert http.parse_options_header( + 'form-data; name="file"; filename="t\'es\'t.txt"' + ) == ('form-data', {'name': 'file', 'filename': "t'es't.txt"}) + assert http.parse_options_header( + 'form-data; name="file"; filename*=UTF-8\'\'"\'🐍\'.txt"' + ) == ('form-data', {'name': 'file', 'filename': u"'🐍'.txt"}) + def test_parse_options_header_broken_values(self): # Issue #995 assert http.parse_options_header(' ') == ('', {}) diff --git a/werkzeug/http.py b/werkzeug/http.py index ee8b9d35f..f026a48ae 100644 --- a/werkzeug/http.py +++ b/werkzeug/http.py @@ -62,12 +62,30 @@ '^_`abcdefghijklmnopqrstuvwxyz|~') _etag_re = re.compile(r'([Ww]/)?(?:"(.*?)"|(.*?))(?:\s*,\s*|$)') _unsafe_header_chars = set('()<>@,;:\"/[]?={} \t') -_quoted_string_re = r'"[^"\\]*(?:\\.[^"\\]*)*"' -_option_header_piece_re = re.compile( - r';\s*(%s|[^\s;,=\*]+)\s*' - r'(?:\*?=\s*(?:([^\s]+?)\'([^\s]*?)\')?(%s|[^;,]+)?)?\s*' % - (_quoted_string_re, _quoted_string_re) -) +_option_header_piece_re = re.compile(r''' + ;\s* + (?P + "[^"\\]*(?:\\.[^"\\]*)*" # quoted string + | + [^\s;,=*]+ # token + ) + \s* + (?: # optionally followed by =value + (?: # equals sign, possibly with encoding + \*\s*=\s* # * indicates extended notation + (?P[^\s]+?) + '(?P[^\s]*?)' + | + =\s* # basic notation + ) + (?P + "[^"\\]*(?:\\.[^"\\]*)*" # quoted string + | + [^;,]+ # token + )? + )? + \s* +''', flags=re.VERBOSE) _option_header_start_mime_type = re.compile(r',\s*([^;,\s]+)([;,]\s*.+)?') _entity_headers = frozenset([