summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJordan Cook <jordan.cook@pioneer.com>2022-04-10 12:15:46 -0500
committerJordan Cook <jordan.cook@pioneer.com>2022-04-10 14:23:43 -0500
commitd39fbfac0192fc9a2dc825dc17ede29776863f5f (patch)
treef222add93c23c4fabd68be204da86f8eb616ad90
parent4a593b0c16aa96d5912fb6605dec46b0dc4bf66e (diff)
downloadrequests-cache-d39fbfac0192fc9a2dc825dc17ede29776863f5f.tar.gz
Add default list of ignored_parameters for most common authentication params/headers
-rw-r--r--HISTORY.md4
-rw-r--r--docs/user_guide/security.md7
-rw-r--r--requests_cache/cache_keys.py5
-rw-r--r--requests_cache/session.py6
-rw-r--r--requests_cache/settings.py5
-rw-r--r--tests/unit/test_session.py49
6 files changed, 56 insertions, 20 deletions
diff --git a/HISTORY.md b/HISTORY.md
index a9aea84..663062e 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -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'