Repository: pallets/werkzeug (python)

Issue:
multipart disposition parameter parser is case-sensitive in violation of RFC6266
I was trying to implement a file uploader for a very-legacy system that is using a `content-disposition: form-data; name="message"; fileName="sample.txt"` header in multipart/form-data uploads.

Flask/werkzeug/sansio is [only looking for a `filename` extra](https://github.com/pallets/werkzeug/blob/main/src/werkzeug/sansio/multipart.py#L167), so it misses the `fileName` extra.

Therefore, the files are misdetected as fields instead of as files.

However, [RFC6266 §4.3](https://datatracker.ietf.org/doc/html/rfc6266#section-4.3) says that

> The parameters "filename" and "filename*", to be matched case-insensitively, provide information on how to construct a filename for storing the message payload.

Minimal example:

```py
from flask import Flask, request, make_response

app = Flask(__name__)

@app.route('/',methods = ['POST', 'GET'])
def sendmail():
    if request.method == 'POST':
        print('files', request.files)
        print('form', request.form)
        return make_response("Yay", 201)
    return make_response("Nay", 200)

if __name__ == '__main__':
    app.run(debug = True)
```

Payload to trigger the issue:
```http
POST / HTTP/1.0
Content-Type: multipart/form-data; boundary=---------------------------7d93b9550d4a
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, */*
Content-Length: 350

-----------------------------7d93b9550d4a
content-disposition: form-data; name="message"; fileName="sample.txt"
content-Type: multipart/form-data;

message

-----------------------------7d93b9550d4a
content-disposition: form-data; name="binary"; fileName="none.jpg"
content-Type: image/jpeg;


-----------------------------7d93b9550d4a--

```

Save the above into request.http and run to test:
```
nc 127.0.0.1 5000 < request.http
```

Expected output: request.files has two elements, "message" and "binary"

Actual output: request.form has two elements, "message" and "binary", request.files has none


Environment:

- Python version: 3.9.10
- Werkzeug version: 2.1.2

P.S: the legacy system I'm using fails to submit an epilogue, instead sending a regular boundary (I've fixed that in the above example), causing the parsing to [fail with a ValueError](https://github.com/pallets/werkzeug/blob/main/src/werkzeug/sansio/multipart.py#L212-L213) which is however silently ignored somewhere upstack, resulting in a "successful" parsing of the form that has neither form fields nor files on it. I'm not sure if this silent ignoring of a form error should be reported as a separate bug.

Pull request description:
Lowercase the parsed options names
I cannot find a RFC that indicates that the option names are case
sensitive whereas RFC6266 states that the `filename` option name is
case insensitive. Therefore the best user experience is to lowercase
the names.

This will cause breaking changes if case sensitivity is relied upon,
and hence users will need to use the lowercase name if so.

closes #2422 

Checklist:

- [x] Add tests that demonstrate the correct behavior of the change. Tests should fail without the change.
- [x] Add or update relevant docs, in the docs folder and in code.
- [x] Add an entry in `CHANGES.rst` summarizing the change and linking to the issue.
- [x] Add `.. versionchanged::` entries in any relevant code docs.
- [x] Run `pre-commit` hooks and fix any issues.
- [x] Run `pytest` and `tox`, no tests failed.


Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index 749f6563..c5247876 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -9,7 +9,9 @@ Version 2.2.0
 -   Fix compatibility with Python 3.11 by ensuring that ``end_lineno``
     and ``end_col_offset`` are present on AST nodes. :issue:`2425`
 -   Add a new faster matching router based on a state
-    machine. :pr:`2433`.
+    machine. :pr:`2433`
+-   Names within options headers are always converted to lowercase. This
+    matches :rfc:`6266` that the case is not relevant. :issue:`2442`
 
 
 Version 2.1.2
diff --git a/src/werkzeug/http.py b/src/werkzeug/http.py
index 93699001..3d0fba90 100644
--- a/src/werkzeug/http.py
+++ b/src/werkzeug/http.py
@@ -390,6 +390,9 @@ def parse_options_header(
 
     :param value: The header value to parse.
 
+    .. versionchanged:: 2.2
+        Option names are always converted to lowercase.
+
     .. versionchanged:: 2.1
         The ``multiple`` parameter is deprecated and will be removed in
         Werkzeug 2.2.
@@ -440,7 +443,7 @@ def parse_options_header(
                 if not encoding:
                     encoding = continued_encoding
                 continued_encoding = encoding
-            option = unquote_header_value(option)
+            option = unquote_header_value(option).lower()
 
             if option_value is not None:
                 option_value = unquote_header_value(option_value, option == "filename")


Test patch:
diff --git a/tests/test_http.py b/tests/test_http.py
index 5936bfa5..623114cb 100644
--- a/tests/test_http.py
+++ b/tests/test_http.py
@@ -358,6 +358,10 @@ class TestHTTPUtility:
         assert http.parse_options_header(" , a ") == ("", {})
         assert http.parse_options_header(" ; a ") == ("", {})
 
+    def test_parse_options_header_case_insensitive(self):
+        _, options = http.parse_options_header(r'something; fileName="File.ext"')
+        assert options["filename"] == "File.ext"
+
     def test_dump_options_header(self):
         assert http.dump_options_header("foo", {"bar": 42}) == "foo; bar=42"
         assert http.dump_options_header("foo", {"bar": 42, "fizz": None}) in (