blob: 056b86ec6b047a496e299e72ead33be475224e2f [file] [view] [edit]
(security)=
# {fas}`lock` Security
This page contains some security recommendations and guidelines for advanced use cases.
## Shared Server Environment
requests-cache is mainly intended for use as a **private, client-side, single-user cache**,
analogous to a browser cache. It should not be used as a public/proxy cache (like Squid or Varnish).
For a purely server-side use case (i.e., caching responses to incoming requests), your best option
will likely be a purpose-built {ref}`server cache <server-cache>` that integrates with your web
framework of choice.
Applications that are both HTTP clients and servers can use requests-cache, but some extra
consideration is required. In particular, if your server is shared among multiple principals (users,
applications, tenants, etc.), you will need to consider the following:
(default-filter-params)=
### 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 (JSON or form
encoding), and headers. Ignored params will be both omitted from request matching and redacted from
the cache.
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)
* `api_key` request param (used by OpenAPI spec)
* `X-API-KEY` header (used by OpenAPI spec)
* `X-Access-Token`/`X-Auth-Token` headers (some JWT implementations)
To explicitly ignore + redact a different parameter or header:
```python
>>> from requests_cache import CachedSession
>>> session = CachedSession(ignored_parameters=['X-Nonstandard-Credential'])
```
To append to the default list instead of replacing it:
```python
>>> from requests_cache import CachedSession, DEFAULT_IGNORED_PARAMS
>>> ignored_parameters = list(DEFAULT_IGNORED_PARAMS) + ['X-Nonstandard-Credential']
>>> session = CachedSession(ignored_parameters=ignored_parameters)
```
However, there are cases where you would _not_ want to exclude this information from the cache;
see section below.
### Auth-gated content
:::{warning}
Authenticated requests for multiple users must be handled carefully to avoid exposing authenticated
content from one user (or other principal) to an unintended one.
:::
This can happen if there is no {ref}`matching` information available to distinguish one user's
request from another. Since requests-cache only operates at the HTTP level, it has no knowledge of
users or other app/framework-specific request context, aside from what is passed via request params
and headers.
To protect against the most common cases of this, requests-cache will refuse to serve a cached
request containing a header that is in both `ignored_parameters` and `Vary` (for example,
`Authorization` + `Vary: Authorization`).
It's preferable to handle this proactively in your application, though. Options include:
* Use a separate cache per user
* Exclude authenticated requests completely (via any combination of {ref}`filtering` features)
* Pass additional headers or params solely for request matching purposes
* Always match on authentication headers using `match_headers` and (if applicable) overriding
`ignored_parameters`. If your cache backend is secured, you can choose to not redact auth
headers/params from the cache.
Example: In the case of `Authorization` with a JWT, this would make the tradeoff of storing the
token in the cache but making it usable for request matching (i.e., store a separate cached response
per token value).
```python
session = CachedSession(
match_headers=['Authorization'], # Make this part of the cache key, even if not specified by Vary
ignored_parameters=[], # Remove it from default ignored/redacted params
)
```
## Pickle Vulnerabilities
:::{warning}
The python `pickle` module has [known security vulnerabilities](https://docs.python.org/3/library/pickle.html),
potentially leading to code execution when deserializing data.
This means it should only be used to deserialize data that you trust hasn't been tampered with.
:::
### Safe Pickling With Untrusted Data
If you're working with untrusted data, consider using one of the other supported {ref}`serializers`
instead of pickle.
Since this isn't always possible, requests-cache can optionally use
[itsdangerous](https://itsdangerous.palletsprojects.com) to add a layer of security around these operations.
It works by signing serialized data with a secret key that you control. Then, if the data is tampered
with, the signature check fails and raises an error.
Optional dependencies can be installed with:
```bash
pip install itsdangerous
```
### Creating and Storing a Secret Key
To enable this behavior, first create a secret key, which can be any `str` or `bytes` object.
One common pattern for handling this is to store it wherever you store the rest of your credentials
([Linux keyring](https://itsfoss.com/ubuntu-keyring),
[macOS keychain](https://support.apple.com/guide/mac-help/use-keychains-to-store-passwords-mchlf375f392/mac),
[password database](https://keepassxc.org), etc.),
set it in an environment variable, and then read it in your application:
```python
>>> import os
>>> secret_key = os.environ['SECRET_KEY']
```
Alternatively, you can use the [keyring](https://keyring.readthedocs.io) package to read the key
directly:
```python
>>> import keyring
>>> secret_key = keyring.get_password('requests-cache-example', 'secret_key')
```
### Signing Cached Responses
Once you have your key, create a {py:func}`.safe_pickle_serializer` with it:
```python
>>> from requests_cache import CachedSession, safe_pickle_serializer
>>> serializer = safe_pickle_serializer(secret_key=secret_key)
>>> session = CachedSession(serializer=serializer)
>>> session.get('https://httpbin.org/get')
```
:::{note}
You can also make your own {ref}`custom-serializers`, if you would like more control over how
responses are serialized.
:::
You can verify that it's working by modifying the cached item (*without* your key):
```python
>>> serializer_2 = safe_pickle_serializer(secret_key='a different key')
>>> session_2 = CachedSession(serializer=serializer_2)
>>> cache_key = list(session_2.cache.responses.keys())[0]
>>> session_2.cache.responses[cache_key] = 'exploit!'
```
Then, if you try to get that cached response again (*with* your key), you will get an error:
```python
>>> session.get('https://httpbin.org/get')
BadSignature: Signature b'iFNmzdUOSw5vqrR9Cb_wfI1EoZ8' does not match
```