Repository navigation
Binärantworten nur mit Typ und Größe loggen #4164
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,6 @@ | ||
| import copy | ||
| import logging | ||
| from requests import Session | ||
| from requests import Response, Session | ||
| import urllib3 | ||
| from functools import wraps | ||
| import warnings | ||
|
|
@@ -47,8 +47,22 @@ def __deepcopy__(self, memo): | |
| return new_copy | ||
|
|
||
|
|
||
| def _log_response(response: Response, *args, **kwargs) -> None: | ||
| if not log.isEnabledFor(logging.DEBUG): | ||
| return | ||
| content_type = response.headers.get('Content-Type', '').split(';', 1)[0].strip().lower() | ||
| text = (content_type.startswith('text/') or | ||
| content_type in ('application/json', 'application/xml', 'application/x-www-form-urlencoded') or | ||
| (content_type.startswith('application/') and content_type.endswith(('+json', '+xml')))) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Medium · Text response bodies disappear from logs When a response uses an unlisted textual application/* media type such as Classify textual application/* media types as text instead of limiting them to the listed types and JSON/XML suffixes. Prompt for AI agentsIn `packages/modules/common/req.py` at line 56:
**Text response bodies disappear from logs**
When a response uses an unlisted textual application/* media type such as `application/javascript`, `_log_response` classifies the response as binary and logs only its type and size, so callers cannot inspect the body in debug logs.
Classify textual application/* media types as text instead of limiting them to the listed types and JSON/XML suffixes. |
||
| # The portal can omit/mislabel the content type of ZIP downloads. | ||
| if not text or response.content.startswith((b'PK\x03\x04', b'PK\x05\x06', b'PK\x07\x08')): | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Medium · Binary payloads are written to logs When non-ZIP binary data is labeled with a text/* content type, Detect non-ZIP binary payloads before logging Prompt for AI agentsIn `packages/modules/common/req.py` at line 58:
**Binary payloads are written to logs**
When non-ZIP binary data is labeled with a text/* content type, `_log_response` trusts the text media type and logs `response.text`, decoding and emitting the binary payload instead of metadata; callers see the binary payload in debug logs.
Detect non-ZIP binary payloads before logging `response.text` and log only their type and size.There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Medium · Prefixed ZIP contents enter logs When a ZIP download has a prepended executable and is labeled with a text-like Content-Type, Detect ZIP archives with a format-aware check that supports prepended data before logging their contents. Prompt for AI agentsIn `packages/modules/common/req.py` at line 58:
**Prefixed ZIP contents enter logs**
When a ZIP download has a prepended executable and is labeled with a text-like Content-Type, `_log_response` recognizes ZIP data only when a ZIP signature starts at byte zero, so a self-extracting ZIP with a prepended executable and a text-like Content-Type falls through to `response.text` and its binary body appears in debug logs.
Detect ZIP archives with a format-aware check that supports prepended data before logging their contents. |
||
| log.debug("Get-Response: binary data (%s, %s bytes)", content_type or 'unknown type', len(response.content)) | ||
| else: | ||
| log.debug("Get-Response: %s", response.text) | ||
|
|
||
|
|
||
| def get_http_session() -> CustomSession: | ||
| session = CustomSession() | ||
| session.hooks['response'].append(lambda r, *args, **kwargs: r.raise_for_status()) | ||
| session.hooks['response'].append(lambda r, *args, **kwargs: log.debug("Get-Response: " + r.text)) | ||
| session.hooks['response'].append(_log_response) | ||
| return session | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| import io | ||
| import logging | ||
| import zipfile | ||
| from unittest.mock import Mock, PropertyMock | ||
|
|
||
| import pytest | ||
| from requests import HTTPError, Response | ||
|
|
||
| from modules.common import req | ||
|
|
||
|
|
||
| @pytest.mark.parametrize('content_type', ['application/zip', 'application/octet-stream', '', 'text/plain']) | ||
| def test_zip_response_logs_metadata_without_decoding_or_changing_body(requests_mock, caplog, content_type): | ||
| archive = io.BytesIO() | ||
| with zipfile.ZipFile(archive, 'w') as handle: | ||
| handle.writestr('vehicle.json', '{"soc": 55}') | ||
| body = archive.getvalue() | ||
| requests_mock.get('https://example.org/data', content=body, headers={'Content-Type': content_type}) | ||
| caplog.set_level(logging.DEBUG, logger=req.__name__) | ||
| with req.get_http_session() as session: | ||
| response = session.get('https://example.org/data') | ||
| assert response.content == body | ||
| assert 'binary data' in caplog.text | ||
| assert f'{len(body)} bytes' in caplog.text | ||
| assert 'vehicle.json' not in caplog.text | ||
| assert '"soc"' not in caplog.text | ||
|
|
||
|
|
||
| @pytest.mark.parametrize('content_type', [ | ||
| 'application/json', 'text/plain', 'text/html; charset=utf-8', 'application/xml', | ||
| 'application/problem+json', 'application/vnd.example+xml', 'application/x-www-form-urlencoded', | ||
| ' Application/JSON ; charset=UTF-8', | ||
| ]) | ||
| def test_text_response_is_still_logged(requests_mock, caplog, content_type): | ||
| body = '{"soc": 55}' | ||
| requests_mock.get('https://example.org/data', text=body, headers={'Content-Type': content_type}) | ||
| caplog.set_level(logging.DEBUG, logger=req.__name__) | ||
| with req.get_http_session() as session: | ||
| response = session.get('https://example.org/data') | ||
| assert response.json() == {'soc': 55} | ||
| assert 'Get-Response: ' + body in caplog.text | ||
|
|
||
|
|
||
| @pytest.mark.parametrize('content_type', [ | ||
| 'application/octet-stream', 'application/x-gzip', 'application/x-7z-compressed', | ||
| 'application/vnd.example.binary', 'image/png', 'audio/mpeg', 'video/mp4', 'font/woff2', | ||
| 'application/pdf', '', None, | ||
| ]) | ||
| def test_binary_content_type_does_not_access_response_text(caplog, content_type): | ||
| headers = {'Content-Type': content_type} if content_type is not None else {} | ||
| response = Mock(spec=Response, headers=headers, content=b'\x00\xff') | ||
| type(response).text = PropertyMock(side_effect=AssertionError('Binary body must not be decoded')) | ||
| caplog.set_level(logging.DEBUG, logger=req.__name__) | ||
| req._log_response(response) | ||
| assert f'binary data ({content_type or "unknown type"}, 2 bytes)' in caplog.text | ||
|
|
||
|
|
||
| @pytest.mark.parametrize('signature', [b'PK\x03\x04', b'PK\x05\x06', b'PK\x07\x08']) | ||
| def test_mislabeled_zip_does_not_access_response_text(caplog, signature): | ||
| response = Mock(spec=Response, headers={'Content-Type': 'text/plain'}, content=signature) | ||
| type(response).text = PropertyMock(side_effect=AssertionError('ZIP body must not be decoded')) | ||
| caplog.set_level(logging.DEBUG, logger=req.__name__) | ||
| req._log_response(response) | ||
| assert 'binary data (text/plain, 4 bytes)' in caplog.text | ||
|
|
||
|
|
||
| def test_disabled_debug_does_not_read_response(caplog): | ||
| caplog.set_level(logging.INFO, logger=req.__name__) | ||
| req._log_response(object()) | ||
|
|
||
|
|
||
| def test_error_status_still_raises_with_full_response(requests_mock): | ||
| requests_mock.get('https://example.org/data', status_code=429, text='Too many requests') | ||
| with req.get_http_session() as session: | ||
| with pytest.raises(HTTPError) as error: | ||
| session.get('https://example.org/data') | ||
| assert error.value.response.status_code == 429 | ||
| assert error.value.response.text == 'Too many requests' |
Uh oh!
There was an error while loading. Please reload this page.