Fail on bytes/string comparisons
MODIFY
noxfile.py
noxfile.py
@@ -14,7 +14,11 @@ SOURCE_FILES = [
]
-def tests_impl(session: nox.Session, extras: str = "socks,secure,brotli") -> None:
+def tests_impl(
+ session: nox.Session,
+ extras: str = "socks,secure,brotli",
+ byte_string_comparisons: bool = True,
+) -> None:
# Install deps and the package itself.
session.install("-r", "dev-requirements.txt")
session.install(f".[{extras}]")
@@ -33,6 +37,9 @@ def tests_impl(session: nox.Session, extras: str = "socks,secure,brotli") -> Non
# and collapse them into src/urllib3/__init__.py.
session.run(
+ "python",
+ *(("-bb",) if byte_string_comparisons else ()),
+ "-m",
"coverage",
"run",
"--parallel-mode",
@@ -75,7 +82,7 @@ def test_brotlipy(session: nox.Session) -> None:
'brotlicffi' that we still don't blow up.
"""
session.install("brotlipy")
- tests_impl(session, extras="socks,secure")
+ tests_impl(session, extras="socks,secure", byte_string_comparisons=False)
def git_clone(session: nox.Session, git_url: str) -> None:

MODIFY
src/urllib3/_collections.py
src/urllib3/_collections.py
@@ -256,6 +256,9 @@ class HTTPHeaderDict(MutableMapping[str, str]):
self.extend(kwargs)
def __setitem__(self, key: str, val: str) -> None:
+ # avoid a bytes/str comparison by decoding before httplib
+ if isinstance(key, bytes):
+ key = key.decode("latin-1")
self._container[key.lower()] = [key, val]
def __getitem__(self, key: str) -> str:
@@ -307,6 +310,9 @@ class HTTPHeaderDict(MutableMapping[str, str]):
>>> headers['foo']
'bar, baz'
"""
+ # avoid a bytes/str comparison by decoding before httplib
+ if isinstance(key, bytes):
+ key = key.decode("latin-1")
key_lower = key.lower()
new_vals = [key, val]
# Keep the common case aka no item present as fast as possible

MODIFY
src/urllib3/connection.py
src/urllib3/connection.py
@@ -274,6 +274,14 @@ class HTTPConnection(_HTTPConnection):
else:
# Avoid modifying the headers passed into .request()
headers = copy(headers)
+ # Don't send bytes keys to httplib to avoid bytes/str comparison
+ # HTTPHeaderDict is already safe, but other types are not
+ for key, value in list(headers.items()):
+ if isinstance(key, bytes):
+ headers.pop(key)
+ # httplib would have decoded to latin-1 anyway
+ headers[key.decode("latin-1")] = value
+
if "user-agent" not in (to_str(k.lower()) for k in headers):
updated_headers = {"User-Agent": _get_default_user_agent()}
updated_headers.update(headers)

MODIFY
src/urllib3/util/ssl_.py
src/urllib3/util/ssl_.py
@@ -142,7 +142,7 @@ def assert_fingerprint(cert: Optional[bytes], fingerprint: str) -> None:
if not hmac.compare_digest(cert_digest, fingerprint_bytes):
raise SSLError(
- f'Fingerprints did not match. Expected "{fingerprint}", got "{cert_digest.hex()}".'
+ f'Fingerprints did not match. Expected "{fingerprint}", got "{cert_digest.hex()}"'
)

MODIFY
test/test_collections.py
test/test_collections.py
@@ -200,7 +200,8 @@ class TestHTTPHeaderDict:
def test_setitem(self, d):
d["Cookie"] = "foo"
- assert d["cookie"] == "foo"
+ d[b"Cookie"] = "bar" # bytes get converted to str
+ assert d["cookie"] == "bar"
d["cookie"] = "with, comma"
assert d.getlist("cookie") == ["with, comma"]
@@ -222,7 +223,7 @@ class TestHTTPHeaderDict:
def test_add_comma_separated_multiheader(self, d):
d.add("bar", "foo")
- d.add("BAR", "bar")
+ d.add(b"BAR", "bar") # bytes get converted to str
d.add("Bar", "asdf")
assert d.getlist("bar") == ["foo", "bar", "asdf"]
assert d["bar"] == "foo, bar, asdf"

MODIFY
test/with_dummyserver/test_https.py
test/with_dummyserver/test_https.py
@@ -429,7 +429,12 @@ class TestHTTPS(HTTPSDummyServerTestCase):
"AA:AA:AA:AA:AA:AAAA:AA:AAAA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA"
)
e = _test_request(https_pool)
- assert "Fingerprints did not match." in str(e)
+ expected = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+ got = "728b554c9afc1e88a11cad1bb2e7cc3edbc8f98a"
+ assert (
+ str(e)
+ == f'Fingerprints did not match. Expected "{expected}", got "{got}"'
+ )
# Uneven length
https_pool.assert_fingerprint = "AA:A"

Fail on bytes/string comparisons