-
Notifications
You must be signed in to change notification settings - Fork 49
Description
Summary
GCS blob downloads fail with TypeError: _GzipDecoder.decompress() got an unexpected keyword argument 'max_length' when using urllib3>=2.6.0. The _GzipDecoder class in google/resumable_media/requests/download.py subclasses urllib3.response.GzipDecoder but its decompress() method doesn't accept the max_length keyword argument introduced in urllib3 2.6.0.
Environment details
- OS type and version: Linux (Ubuntu, also reproducible on other platforms)
- Python version: 3.12
- pip version: 24.x
google-resumable-mediaversion: 2.7.2 (and likely all current versions)urllib3version: 2.6.0 or 2.6.1google-cloud-storageversion: 2.19.0
Root Cause
urllib3 2.6.0 (released December 5, 2025) introduced a breaking API change as part of the security fix for CVE-2025-66471 (GHSA-2xpw-w6gg-jr37, severity 8.9 High). This vulnerability allowed decompression bombs to cause DoS through excessive CPU/memory consumption.
The fix added a max_length parameter to decoder decompress() methods. In urllib3/response.py, the _decode() method now calls:
self._decoder.decompress(data, max_length=max_length)However, google-resumable-media's _GzipDecoder subclass overrides decompress() without accepting this parameter:
# google/resumable_media/requests/download.py (current code)
class _GzipDecoder(urllib3.response.GzipDecoder):
def __init__(self, checksum):
super().__init__()
self._checksum = checksum
def decompress(self, data): # ← Missing **kwargs or max_length parameter!
self._checksum.update(data)
return super().decompress(data)The same issue affects _BrotliDecoder in the same file.
Steps to reproduce
- Install
urllib3>=2.6.0(or let it upgrade naturally) - Attempt to download a gzip-encoded blob from Google Cloud Storage
pip install urllib3==2.6.1 google-cloud-storageCode example
from google.cloud import storage
client = storage.Client()
bucket = client.bucket("your-bucket")
blob = bucket.blob("your-gzip-encoded-file")
# This fails with urllib3 2.6.0+
content = blob.download_as_bytes()Stack trace
"Traceback (most recent call last):
^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/contextlib.py", line 81, in inner
return func(*args, **kwds)
^^^^^^^^^^^^^^^^^^^
File "/app/.venv/lib/python3.12/site-packages/google/cloud/storage/blob.py", line 1473, in download_as_bytes
self._prep_and_do_download(
File "/app/.venv/lib/python3.12/site-packages/google/cloud/storage/blob.py", line 4423, in _prep_and_do_download
self._do_download(
File "/app/.venv/lib/python3.12/site-packages/google/cloud/storage/blob.py", line 1045, in _do_download
response = download.consume(transport, timeout=timeout)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/app/.venv/lib/python3.12/site-packages/google/resumable_media/requests/download.py", line 263, in consume
return _request_helpers.wait_and_retry(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/app/.venv/lib/python3.12/site-packages/google/resumable_media/requests/_request_helpers.py", line 155, in wait_and_retry
response = func()
^^^^^^
File "/app/.venv/lib/python3.12/site-packages/google/resumable_media/requests/download.py", line 259, in retriable_request
self._write_to_stream(result)
File "/app/.venv/lib/python3.12/site-packages/google/resumable_media/requests/download.py", line 131, in _write_to_stream
for chunk in body_iter:
^^^^^^^^^
File "/app/.venv/lib/python3.12/site-packages/requests/models.py", line 820, in generate
yield from self.raw.stream(chunk_size, decode_content=True)
File "/app/.venv/lib/python3.12/site-packages/urllib3/response.py", line 1246, in stream
yield from self.read_chunked(amt, decode_content=decode_content)
File "/app/.venv/lib/python3.12/site-packages/urllib3/response.py", line 1414, in read_chunked
decoded = self._decode(
^^^^^^^^^^^^^
File "/app/.venv/lib/python3.12/site-packages/urllib3/response.py", line 642, in _decode
data = self._decoder.decompress(data, max_length=max_length)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: _GzipDecoder.decompress() got an unexpected keyword argument 'max_length'
Suggested Fix
Update _GzipDecoder.decompress() (and _BrotliDecoder.decompress()) to accept and forward the max_length parameter:
class _GzipDecoder(urllib3.response.GzipDecoder):
def __init__(self, checksum):
super().__init__()
self._checksum = checksum
def decompress(self, data, **kwargs): # Accept additional kwargs
self._checksum.update(data)
return super().decompress(data, **kwargs) # Forward them to parentAlternatively, explicitly handle max_length:
def decompress(self, data, max_length=0):
self._checksum.update(data)
return super().decompress(data, max_length=max_length)Workaround
Pin urllib3 to versions before 2.6.0:
urllib3>=2.0,<2.6
Additional Context
- urllib3 2.6.0 release: https://github.com/urllib3/urllib3/releases/tag/2.6.0
- CVE-2025-66471 security advisory: GHSA-2xpw-w6gg-jr37
- The
_GzipDecoderclass was introduced in PR Add requests/urllib3 work-around for intercepting gzipped bytes. #36 to intercept gzipped bytes for checksum validation - This issue also affects
google._async_resumable_media.requests.downloadwhich has an identical_GzipDecoderimplementation