Repository: pallets/werkzeug

Issue:
Parsing request bodies containing file uploads with a filename containing two single quotes fails
I can reproduce this in Flask but I'm pretty sure this is a bug in Werkzeug.


```
In [6]: app = Flask(__name__)
   ...: @app.route('/upload', methods=('POST',))
   ...: def upload():
   ...:     print request.form
   ...:     print request.files
   ...:     return 'blah'
   ...: app.run(port=31337)
```

    http -f post http://127.0.0.1:31337/upload "file@/tmp/t'es't.txt"

```
Traceback (most recent call last):
  File "/home/adrian/dev/indico/env/lib/python2.7/site-packages/flask/app.py", line 1982, in wsgi_app
    response = self.full_dispatch_request()
  File "/home/adrian/dev/indico/env/lib/python2.7/site-packages/flask/app.py", line 1614, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/home/adrian/dev/indico/env/lib/python2.7/site-packages/flask/app.py", line 1517, in handle_user_exception
    reraise(exc_type, exc_value, tb)
  File "/home/adrian/dev/indico/env/lib/python2.7/site-packages/flask/app.py", line 1612, in full_dispatch_request
    rv = self.dispatch_request()
  File "/home/adrian/dev/indico/env/lib/python2.7/site-packages/flask/app.py", line 1598, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
  File "<ipython-input-6-c04743f50a59>", line 4, in upload
    print request.form
  File "/home/adrian/dev/indico/env/lib/python2.7/site-packages/werkzeug/local.py", line 347, in __getattr__
    return getattr(self._get_current_object(), name)
  File "/home/adrian/dev/indico/env/lib/python2.7/site-packages/werkzeug/utils.py", line 73, in __get__
    value = self.func(obj)
  File "/home/adrian/dev/indico/env/lib/python2.7/site-packages/werkzeug/wrappers.py", line 523, in form
    self._load_form_data()
  File "/home/adrian/dev/indico/env/lib/python2.7/site-packages/flask/wrappers.py", line 185, in _load_form_data
    RequestBase._load_form_data(self)
  File "/home/adrian/dev/indico/env/lib/python2.7/site-packages/werkzeug/wrappers.py", line 371, in _load_form_data
    mimetype, content_length, options)
  File "/home/adrian/dev/indico/env/lib/python2.7/site-packages/werkzeug/formparser.py", line 195, in parse
    content_length, options)
  File "/home/adrian/dev/indico/env/lib/python2.7/site-packages/werkzeug/formparser.py", line 100, in wrapper
    return f(self, stream, *args, **kwargs)
  File "/home/adrian/dev/indico/env/lib/python2.7/site-packages/werkzeug/formparser.py", line 212, in _parse_multipart
    form, files = parser.parse(stream, boundary, content_length)
  File "/home/adrian/dev/indico/env/lib/python2.7/site-packages/werkzeug/formparser.py", line 519, in parse
    return self.cls(form), self.cls(files)
  File "/home/adrian/dev/indico/env/lib/python2.7/site-packages/werkzeug/datastructures.py", line 406, in __init__
    for key, value in mapping or ():
  File "/home/adrian/dev/indico/env/lib/python2.7/site-packages/werkzeug/formparser.py", line 517, in <genexpr>
    form = (p[1] for p in formstream if p[0] == 'form')
  File "/home/adrian/dev/indico/env/lib/python2.7/site-packages/werkzeug/formparser.py", line 476, in parse_parts
    for ellt, ell in self.parse_lines(file, boundary, content_length):
  File "/home/adrian/dev/indico/env/lib/python2.7/site-packages/werkzeug/formparser.py", line 403, in parse_lines
    disposition, extra = parse_options_header(disposition)
  File "/home/adrian/dev/indico/env/lib/python2.7/site-packages/werkzeug/http.py", line 368, in parse_options_header
    option_value = _unquote(option_value).decode(encoding)
LookupError: unknown encoding: "t
```

Installed versions: Flask==0.12.2, Werkzeug==0.12.2


Discussion:
#1091 sounds related.

Resolving patch:
diff --git a/werkzeug/http.py b/werkzeug/http.py
--- 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<key>
+        "[^"\\]*(?:\\.[^"\\]*)*"  # quoted string
+    |
+        [^\s;,=*]+  # token
+    )
+    \s*
+    (?:  # optionally followed by =value
+        (?:  # equals sign, possibly with encoding
+            \*\s*=\s*  # * indicates extended notation
+            (?P<encoding>[^\s]+?)
+            '(?P<language>[^\s]*?)'
+        |
+            =\s*  # basic notation
+        )
+        (?P<value>
+            "[^"\\]*(?:\\.[^"\\]*)*"  # quoted string
+        |
+            [^;,]+  # token
+        )?
+    )?
+    \s*
+''', flags=re.VERBOSE)
 _option_header_start_mime_type = re.compile(r',\s*([^;,\s]+)([;,]\s*.+)?')
 
 _entity_headers = frozenset([


Test patch:
diff --git a/tests/test_http.py b/tests/test_http.py
--- 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(' ') == ('', {})