Repository: pallets/werkzeug (python)

Issue:
Inconsistent handling of newlines in Headers object
Werkzeug's `Headers` object is inconsistent in its handling of newline characters.  These characters are allowed in the constructor but forbidden in the `add()` method.

**Steps to reproduce:**
```
>>> from werkzeug import Headers
>>> h1 = Headers([('X-Example', 'foo\r\n bar')])
>>> h2 = Headers()
>>> h2.add('X-Example', 'foo\r\n bar')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/.../venv3/lib/python3.6/site-packages/werkzeug/datastructures.py", line 1166, in add
    self._validate_value(_value)
  File "/home/.../venv3/lib/python3.6/site-packages/werkzeug/datastructures.py", line 1173, in _validate_value
    raise ValueError('Detected newline in header value.  This is '
ValueError: Detected newline in header value.  This is a potential security problem
```

**Expected result:**
Both operations have the same result, whether a `ValueError` or success.

**Actual result:**
`Headers` constructor allows newlines while `Headers.add()` raises `ValueError`.

Pull request:
Fix handling of defaults in Header objects
Fix the handling of defaults passed to the `Headers` constructor. It now has uniform behavior with headers added with `Headers.add`

This fix uses `Headers.extend` to add defaults as opposed to `List.extend` used previously. Using the Headers' extend method allows all internal checks and conversions to be done on the default values passed.

For an example, newlines are not allowed in headers. 
```python3
    h1 = Headers([('X-Example', 'foo\r\n bar')])
    h2 = Headers()
    h2.add('X-Example', 'foo\r\n bar')
```
Before this fix `h1` would get created while `h2` would fail. Now they would both fail.

- fixes #1608 

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 8fa1e454..0ddfd874 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -5,6 +5,9 @@ Version 2.1.0
 
 Unreleased
 
+-   Default values passed to ``Headers`` are validated the same way
+    values added later are. :issue:`1608`
+
 
 Version 2.0.2
 -------------
diff --git a/src/werkzeug/datastructures.py b/src/werkzeug/datastructures.py
index d2fe2b8e..64695cb8 100644
--- a/src/werkzeug/datastructures.py
+++ b/src/werkzeug/datastructures.py
@@ -857,12 +857,15 @@ class Headers:
     `None` for ``headers['missing']``, whereas :class:`Headers` will raise
     a :class:`KeyError`.
 
-    To create a new :class:`Headers` object pass it a list or dict of headers
-    which are used as default values.  This does not reuse the list passed
-    to the constructor for internal usage.
+    To create a new ``Headers`` object, pass it a list, dict, or
+    other ``Headers`` object with default values. These values are
+    validated the same way values added later are.
 
     :param defaults: The list of default values for the :class:`Headers`.
 
+    .. versionchanged:: 2.1.0
+        Default values are validated the same as values added later.
+
     .. versionchanged:: 0.9
        This data structure now stores unicode values similar to how the
        multi dicts do it.  The main difference is that bytes can be set as
@@ -876,10 +879,7 @@ class Headers:
     def __init__(self, defaults=None):
         self._list = []
         if defaults is not None:
-            if isinstance(defaults, (list, Headers)):
-                self._list.extend(defaults)
-            else:
-                self.extend(defaults)
+            self.extend(defaults)
 
     def __getitem__(self, key, _get_mode=False):
         if not _get_mode:


Test patch:
diff --git a/tests/test_datastructures.py b/tests/test_datastructures.py
index 7bed93f4..9de02378 100644
--- a/tests/test_datastructures.py
+++ b/tests/test_datastructures.py
@@ -621,6 +621,9 @@ class TestHeaders:
         assert "x-Foo" in headers
         assert "Content-type" in headers
 
+        with pytest.raises(ValueError):
+            headers.add("X-Example", "foo\r\n bar")
+
         headers["Content-Type"] = "foo/bar"
         assert headers["Content-Type"] == "foo/bar"
         assert len(headers.getlist("Content-Type")) == 1
@@ -637,6 +640,10 @@ class TestHeaders:
         headers.add("x", "y", z='"')
         assert headers["x"] == r'y; z="\""'
 
+        # string conversion
+        headers.add("a", 1)
+        assert headers["a"] == "1"
+
     def test_defaults_and_conversion(self):
         # defaults
         headers = self.storage_class(
@@ -657,6 +664,10 @@ class TestHeaders:
         assert headers.setdefault("X-Baz", "nope") == "quux"
         headers.pop("X-Baz")
 
+        # newlines are not allowed in values
+        with pytest.raises(ValueError):
+            self.storage_class([("X-Example", "foo\r\n bar")])
+
         # type conversion
         assert headers.get("x-bar", type=int) == 1
         assert headers.getlist("x-bar", type=int) == [1, 2]
@@ -677,8 +688,10 @@ class TestHeaders:
 
     def test_popping(self):
         headers = self.storage_class([("a", 1)])
-        assert headers.pop("a") == 1
-        assert headers.pop("b", 2) == 2
+        # headers object expect string values. If a non string value
+        # is passed, it tries converting it to a string
+        assert headers.pop("a") == "1"
+        assert headers.pop("b", "2") == "2"
 
         with pytest.raises(KeyError):
             headers.pop("c")