diff options
author | Jordan Cook <jordan.cook@pioneer.com> | 2022-04-10 12:15:46 -0500 |
---|---|---|
committer | Jordan Cook <jordan.cook@pioneer.com> | 2022-04-10 14:23:43 -0500 |
commit | d39fbfac0192fc9a2dc825dc17ede29776863f5f (patch) | |
tree | f222add93c23c4fabd68be204da86f8eb616ad90 | |
parent | 4a593b0c16aa96d5912fb6605dec46b0dc4bf66e (diff) | |
download | requests-cache-d39fbfac0192fc9a2dc825dc17ede29776863f5f.tar.gz |
Add default list of ignored_parameters for most common authentication params/headers
-rw-r--r-- | HISTORY.md | 4 | ||||
-rw-r--r-- | docs/user_guide/security.md | 7 | ||||
-rw-r--r-- | requests_cache/cache_keys.py | 5 | ||||
-rw-r--r-- | requests_cache/session.py | 6 | ||||
-rw-r--r-- | requests_cache/settings.py | 5 | ||||
-rw-r--r-- | tests/unit/test_session.py | 49 |
6 files changed, 56 insertions, 20 deletions
@@ -14,7 +14,7 @@ * The constant `requests_cache.DO_NOT_CACHE` may be used to completely disable caching for a request **Backends:** -* Add `wal` parameter for SQLite backend to enable write-ahead logging +* SQLite: Add a `wal` parameter to enable write-ahead logging **Other features:** * All settings that affect cache behavior can now be accessed and modified via `CachedSession.settings` @@ -27,6 +27,8 @@ * Populate `cache_key` and `expires` for new (non-cached) responses, if it was written to the cache * Add return type hints for all `CachedSession` request methods (`get()`, `post()`, etc.) * Always skip both cache read and write for requests excluded by `allowable_methods` (previously only skipped write) +* Ignore and redact common authentication params and headers (e.g., for OAuth2) by default + * This is simply a default value for `ignored_parameters`, to avoid accidentally storing credentials in the cache **Dependencies:** * Replace `appdirs` with `platformdirs` diff --git a/docs/user_guide/security.md b/docs/user_guide/security.md index cad4d3f..17cf380 100644 --- a/docs/user_guide/security.md +++ b/docs/user_guide/security.md @@ -69,3 +69,10 @@ BadSignature: Signature b'iFNmzdUOSw5vqrR9Cb_wfI1EoZ8' does not match ## Removing Sensitive Info The {ref}`ignored_parameters <filter-params>` option can be used to prevent credentials and other sensitive info from being saved to the cache. It applies to request parameters, body, and headers. + +Some are ignored by default, including: +* `Authorization` header (most authentication systems) +* `access_token` request param (used by OAuth) +* `access_token` in POST body (used by OAuth) +* `X-API-KEY` header (used by OpenAPI spec) +* `api_key` request param (used by OpenAPI spec) diff --git a/requests_cache/cache_keys.py b/requests_cache/cache_keys.py index 8a1582c..99e7904 100644 --- a/requests_cache/cache_keys.py +++ b/requests_cache/cache_keys.py @@ -42,7 +42,7 @@ def create_key( Args: request: Request object to generate a cache key from - ignored_parameters: Request parames, headers, and/or body params to not match against + ignored_parameters: Request paramters, headers, and/or JSON body params to exclude match_headers: Match only the specified headers, or ``True`` to match all headers request_kwargs: Request arguments to generate a cache key from """ @@ -95,8 +95,7 @@ def normalize_request( Args: request: Request object to normalize - ignored_parameters: Request parames, headers, and/or body params to not match against and - to remove from the request + ignored_parameters: Request paramters, headers, and/or JSON body params to exclude """ if isinstance(request, Request): norm_request: AnyPreparedRequest = Session().prepare_request(request) diff --git a/requests_cache/session.py b/requests_cache/session.py index 3a1b1a3..99d0509 100644 --- a/requests_cache/session.py +++ b/requests_cache/session.py @@ -31,6 +31,7 @@ from .models import AnyResponse, CachedResponse, OriginalResponse from .serializers import SerializerPipeline from .settings import ( DEFAULT_CACHE_NAME, + DEFAULT_IGNORED_PARAMS, DEFAULT_METHODS, DEFAULT_STATUS_CODES, CacheSettings, @@ -62,7 +63,7 @@ class CacheMixin(MIXIN_BASE): cache_control: bool = False, allowable_codes: Iterable[int] = DEFAULT_STATUS_CODES, allowable_methods: Iterable[str] = DEFAULT_METHODS, - ignored_parameters: Iterable[str] = None, + ignored_parameters: Iterable[str] = DEFAULT_IGNORED_PARAMS, match_headers: Union[Iterable[str], bool] = False, filter_fn: FilterCallback = None, key_fn: KeyCallback = None, @@ -335,7 +336,8 @@ class CachedSession(CacheMixin, OriginalSession): allowable_methods: Cache only responses for one of these HTTP methods match_headers: Match request headers when reading from the cache; may be either ``True`` or a list of specific headers to match - ignored_parameters: List of request parameters to not match against, and exclude from the cache + ignored_parameters: Request paramters, headers, and/or JSON body params to exclude from both + request matching and cached request data stale_if_error: Return stale cache data if a new request raises an exception filter_fn: Response filtering function that indicates whether or not a given response should be cached. See :ref:`custom-filtering` for details. diff --git a/requests_cache/settings.py b/requests_cache/settings.py index 5a7cc6f..62a6c8b 100644 --- a/requests_cache/settings.py +++ b/requests_cache/settings.py @@ -11,6 +11,9 @@ DEFAULT_CACHE_NAME = 'http_cache' DEFAULT_METHODS = ('GET', 'HEAD') DEFAULT_STATUS_CODES = (200,) +# Default params and/or headers that are excluded from cache keys and redacted from cached responses +DEFAULT_IGNORED_PARAMS = ('Authorization', 'X-API-KEY', 'access_token', 'api_key') + # Signatures for user-provided callbacks FilterCallback = Callable[[Response], bool] KeyCallback = Callable[..., str] @@ -30,7 +33,7 @@ class CacheSettings: disabled: bool = field(default=False) expire_after: ExpirationTime = field(default=None) filter_fn: FilterCallback = field(default=None) - ignored_parameters: Iterable[str] = field(default=None) + ignored_parameters: Iterable[str] = field(default=DEFAULT_IGNORED_PARAMS) key_fn: KeyCallback = field(default=None) match_headers: Union[Iterable[str], bool] = field(default=False) only_if_cached: bool = field(default=False) diff --git a/tests/unit/test_session.py b/tests/unit/test_session.py index 4fa1221..2b30898 100644 --- a/tests/unit/test_session.py +++ b/tests/unit/test_session.py @@ -30,6 +30,10 @@ from tests.conftest import ( MOCKED_URL_REDIRECT_TARGET, ) +# Some tests must disable url normalization to retain the custom `http+mock//` protocol +patch_normalize_url = patch('requests_cache.cache_keys.normalize_url', side_effect=lambda x, y: x) + + # Basic initialization # ----------------------------------------------------- @@ -120,9 +124,9 @@ def test_all_methods__ignored_parameters__not_matched(field, method, mock_sessio """ mock_session.settings.ignored_parameters = ['ignored'] mock_session.settings.match_headers = True - params_1 = {'ignored': 'value_1', 'not_ignored': 'value_1'} - params_2 = {'ignored': 'value_2', 'not_ignored': 'value_1'} - params_3 = {'ignored': 'value_2', 'not_ignored': 'value_2'} + params_1 = {'ignored': 'value_1', 'param': 'value_1'} + params_2 = {'ignored': 'value_2', 'param': 'value_1'} + params_3 = {'ignored': 'value_2', 'param': 'value_2'} assert mock_session.request(method, MOCKED_URL, **{field: params_1}).from_cache is False assert mock_session.request(method, MOCKED_URL, **{field: params_1}).from_cache is True @@ -137,15 +141,15 @@ def test_all_methods__ignored_parameters__redacted(field, method, mock_session): """Test all relevant combinations of methods and data fields. Requests with ignored params should have those values redacted from the cached response. """ - mock_session.settings.ignored_parameters = ['access_token'] - params_1 = {'access_token': 'asdf', 'not_ignored': 'value_1'} + mock_session.settings.ignored_parameters = ['ignored'] + params_1 = {'ignored': 'asdf', 'param': 'value_1'} mock_session.request(method, MOCKED_URL, **{field: params_1}) cached_response = mock_session.request(method, MOCKED_URL, **{field: params_1}) - assert 'access_token' not in cached_response.url - assert 'access_token' not in cached_response.request.url - assert 'access_token' not in cached_response.request.headers - assert 'access_token' not in cached_response.request.body.decode('utf-8') + assert 'ignored' not in cached_response.url + assert 'ignored' not in cached_response.request.url + assert 'ignored' not in cached_response.request.headers + assert 'ignored' not in cached_response.request.body.decode('utf-8') # Variations of relevant request arguments @@ -185,7 +189,8 @@ def test_response_history(mock_session): assert len(mock_session.cache.redirects) == 1 -def test_urls(mock_session): +@patch_normalize_url +def test_urls(mock_normalize_url, mock_session): for url in [MOCKED_URL, MOCKED_URL_JSON, MOCKED_URL_HTTPS]: mock_session.get(url) @@ -442,7 +447,23 @@ def test_allowable_methods(mock_session): assert mock_session.put(MOCKED_URL).from_cache is False -def test_filter_fn(mock_session): +def test_default_ignored_parameters(mock_session): + """Common auth params and headers (for OAuth2, etc.) should be ignored by default""" + mock_session.get( + MOCKED_URL, + params={'access_token': 'token'}, + headers={'Authorization': 'Bearer token'}, + ) + response = mock_session.get(MOCKED_URL) + + assert response.from_cache is True + assert 'access_token' not in response.url + assert 'access_token' not in response.request.url + assert 'Authorization' not in response.request.headers + + +@patch_normalize_url +def test_filter_fn(mock_normalize_url, mock_session): mock_session.settings.filter_fn = lambda r: r.request.url != MOCKED_URL_JSON # This request should be cached @@ -456,7 +477,8 @@ def test_filter_fn(mock_session): assert mock_session.get(MOCKED_URL_JSON).from_cache is False -def test_filter_fn__retroactive(mock_session): +@patch_normalize_url +def test_filter_fn__retroactive(mock_normalize_url, mock_session): """filter_fn should also apply to previously cached responses""" mock_session.get(MOCKED_URL_JSON) mock_session.settings.filter_fn = lambda r: r.request.url != MOCKED_URL_JSON @@ -567,7 +589,8 @@ def test_url_allowlist(mock_session): assert not mock_session.cache.has_url(MOCKED_URL) -def test_remove_expired_responses(mock_session): +@patch_normalize_url +def test_remove_expired_responses(mock_normalize_url, mock_session): unexpired_url = f'{MOCKED_URL}?x=1' mock_session.mock_adapter.register_uri( 'GET', unexpired_url, status_code=200, text='mock response' |