requests-hardened 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2023, Saleor Commerce
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,180 @@
1
+ Metadata-Version: 2.1
2
+ Name: requests-hardened
3
+ Version: 1.0.0
4
+ Summary: A library that overrides the default behaviors of the requests library, and adds new security features.
5
+ License: BSD-3-Clause
6
+ Author: Saleor Commerce
7
+ Author-email: hello@saleor.io
8
+ Requires-Python: >=3.9,<4.0
9
+ Classifier: Development Status :: 5 - Production/Stable
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: BSD License
12
+ Classifier: Natural Language :: English
13
+ Classifier: Operating System :: MacOS :: MacOS X
14
+ Classifier: Operating System :: Microsoft :: Windows
15
+ Classifier: Operating System :: POSIX
16
+ Classifier: Operating System :: POSIX :: BSD
17
+ Classifier: Operating System :: POSIX :: Linux
18
+ Classifier: Programming Language :: Python
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3.9
21
+ Classifier: Programming Language :: Python :: 3.10
22
+ Classifier: Programming Language :: Python :: 3.11
23
+ Classifier: Programming Language :: Python :: 3.12
24
+ Classifier: Programming Language :: Python :: 3.13
25
+ Classifier: Programming Language :: Python :: 3 :: Only
26
+ Classifier: Programming Language :: Python :: Implementation :: CPython
27
+ Classifier: Programming Language :: Python :: Implementation :: PyPy
28
+ Classifier: Topic :: Security
29
+ Requires-Dist: requests (>=2.32.3,<3.0.0)
30
+ Project-URL: Changelog, https://github.com/saleor/requests-hardened/releases/
31
+ Project-URL: Homepage, https://github.com/saleor/requests-hardened/
32
+ Project-URL: Issues, https://github.com/saleor/requests-hardened/issues
33
+ Project-URL: Source, https://github.com/saleor/requests-hardened/
34
+ Description-Content-Type: text/x-rst
35
+
36
+ =================
37
+ requests-hardened
38
+ =================
39
+
40
+ |pypi-latest-version| |pypi-python-versions| |pypi-implementations|
41
+
42
+
43
+ ``requests-hardened`` is a library that overrides the default behaviors of the ``requests``
44
+ library, and adds new security features.
45
+
46
+ Installation
47
+ ============
48
+
49
+ The project is available on PyPI_:
50
+
51
+ .. code-block::
52
+
53
+ pip install requests-hardened
54
+
55
+ Features
56
+ ========
57
+
58
+ - `SSRF Filters`_: blocks private and loopback IP ranges.
59
+ - HTTP Redirects: can be used safely alongside the SSRF filter feature.
60
+ - `Proxy Support`_: proxies can be used in combination with SSRF Filters for a defense in depth.
61
+ - Handy `Overrides of Defaults`_: allows to enforce secure defaults globally, such as to
62
+ mitigate DoS attacks.
63
+
64
+ Overrides of Defaults
65
+ ---------------------
66
+
67
+ This library allows to override some default values from the ``requests`` library
68
+ that can have a security impact:
69
+
70
+ - ``Config.never_redirect = False`` always reject HTTP redirects
71
+ - ``Config.default_timeout = (2, 10)`` sets the default timeout value when no value or ``None`` is passed
72
+ - ``Config.user_agent_override = None`` optional config to override ``User-Agent`` header. When set to ``None``, ``requests`` library will set its `default user-agent <https://github.com/psf/requests/blob/ee93fac6b2f715151f1aa9a1a06ddba9f7dcc59a/src/requests/utils.py#L886-L892>`_.
73
+
74
+ SSRF Filters
75
+ ------------
76
+
77
+ A SSRF IP filter can be used to reject HTTP(S) requests targeting private and loopback
78
+ IP addresses.
79
+
80
+ Settings:
81
+
82
+ - ``Config.ip_filter_enable`` whether or not to filter the IP addresses
83
+ - ``ip_filter_allow_loopback_ips`` whether or not to allow loopback IP addresses
84
+
85
+ Proxy Support
86
+ ^^^^^^^^^^^^^
87
+
88
+ The SSRF IP filter's behavior with proxies are as follows:
89
+
90
+ - **Proxy's IP Address:** does not block private and loopback IP addresses (no filtering).
91
+ Instead, the filter assumes that the proxy URL is never tainted with untrusted
92
+ user input.
93
+ - **Target IP Address (Tunneled HTTP Requests):** by default, the tunneled requests are
94
+ filtered for potential SSRF attacks.
95
+ - **Protocols Supported:** SOCKS4, SOCKS5, HTTP, and HTTPS proxy server protocols are supported.
96
+
97
+ .. note::
98
+
99
+ We rely on the ``requests`` and ``urllib3`` thus the list may change over time.
100
+
101
+ .. warning::
102
+
103
+ For SOCKS4 and SOCKS5, you need to run ``pip install requests[socks]``
104
+
105
+ Example Usage:
106
+
107
+ .. code-block:: python
108
+
109
+ from requests_hardened import Config, Manager
110
+
111
+ http_manager = Manager(
112
+ Config(
113
+ default_timeout=(2, 10),
114
+ never_redirect=False,
115
+ # Enable SSRF IP filter
116
+ ip_filter_enable=True,
117
+ ip_filter_allow_loopback_ips=False,
118
+ )
119
+ )
120
+
121
+ # List of proxies
122
+ proxies = {
123
+ "https": "socks5://127.0.0.1:8888",
124
+ "http": "socks5://127.0.0.1:8888",
125
+ }
126
+
127
+ # Sends the HTTP request using the proxy
128
+ resp = http_manager.send_request("GET", "https://example.com", proxies=proxies)
129
+ print(resp)
130
+
131
+
132
+ .. note::
133
+
134
+ For more details on using proxies with the ``requests`` library, see the `official
135
+ documentation <https://docs.python-requests.org/en/latest/user/advanced/#proxies>`_.
136
+
137
+
138
+ Full Example
139
+ ============
140
+
141
+ .. code-block:: python
142
+
143
+ from requests_hardened import Config, Manager
144
+
145
+ # Creates a global "manager" that can be used to create ``requests.Session``
146
+ # objects with hardening in place.
147
+ http_manager = Manager(
148
+ Config(
149
+ default_timeout=(2, 10),
150
+ never_redirect=False,
151
+ ip_filter_enable=True,
152
+ ip_filter_allow_loopback_ips=False,
153
+ user_agent_override=None
154
+ )
155
+ )
156
+
157
+ # Sends an HTTP request without re-using ``requests.Session``:
158
+ resp = http_manager.send_request("GET", "https://example.com")
159
+ print(resp)
160
+
161
+ # Sends HTTP requests with reusable ``requests.Session``:
162
+ with http_manager.get_session() as sess:
163
+ sess.request("GET", "https://example.com")
164
+ sess.request("POST", "https://example.com", json={"foo": "bar"})
165
+
166
+
167
+ .. _PyPI: https://pypi.org/project/requests-hardened
168
+
169
+ .. |pypi-latest-version| image:: https://img.shields.io/pypi/v/requests-hardened.svg
170
+ :alt: Latest Version
171
+ :target: `PyPI`_
172
+
173
+ .. |pypi-python-versions| image:: https://img.shields.io/pypi/pyversions/requests-hardened.svg
174
+ :alt: Supported Python Versions
175
+ :target: `PyPI`_
176
+
177
+ .. |pypi-implementations| image:: https://img.shields.io/pypi/implementation/requests-hardened.svg
178
+ :alt: Supported Implementations
179
+ :target: `PyPI`_
180
+
@@ -0,0 +1,144 @@
1
+ =================
2
+ requests-hardened
3
+ =================
4
+
5
+ |pypi-latest-version| |pypi-python-versions| |pypi-implementations|
6
+
7
+
8
+ ``requests-hardened`` is a library that overrides the default behaviors of the ``requests``
9
+ library, and adds new security features.
10
+
11
+ Installation
12
+ ============
13
+
14
+ The project is available on PyPI_:
15
+
16
+ .. code-block::
17
+
18
+ pip install requests-hardened
19
+
20
+ Features
21
+ ========
22
+
23
+ - `SSRF Filters`_: blocks private and loopback IP ranges.
24
+ - HTTP Redirects: can be used safely alongside the SSRF filter feature.
25
+ - `Proxy Support`_: proxies can be used in combination with SSRF Filters for a defense in depth.
26
+ - Handy `Overrides of Defaults`_: allows to enforce secure defaults globally, such as to
27
+ mitigate DoS attacks.
28
+
29
+ Overrides of Defaults
30
+ ---------------------
31
+
32
+ This library allows to override some default values from the ``requests`` library
33
+ that can have a security impact:
34
+
35
+ - ``Config.never_redirect = False`` always reject HTTP redirects
36
+ - ``Config.default_timeout = (2, 10)`` sets the default timeout value when no value or ``None`` is passed
37
+ - ``Config.user_agent_override = None`` optional config to override ``User-Agent`` header. When set to ``None``, ``requests`` library will set its `default user-agent <https://github.com/psf/requests/blob/ee93fac6b2f715151f1aa9a1a06ddba9f7dcc59a/src/requests/utils.py#L886-L892>`_.
38
+
39
+ SSRF Filters
40
+ ------------
41
+
42
+ A SSRF IP filter can be used to reject HTTP(S) requests targeting private and loopback
43
+ IP addresses.
44
+
45
+ Settings:
46
+
47
+ - ``Config.ip_filter_enable`` whether or not to filter the IP addresses
48
+ - ``ip_filter_allow_loopback_ips`` whether or not to allow loopback IP addresses
49
+
50
+ Proxy Support
51
+ ^^^^^^^^^^^^^
52
+
53
+ The SSRF IP filter's behavior with proxies are as follows:
54
+
55
+ - **Proxy's IP Address:** does not block private and loopback IP addresses (no filtering).
56
+ Instead, the filter assumes that the proxy URL is never tainted with untrusted
57
+ user input.
58
+ - **Target IP Address (Tunneled HTTP Requests):** by default, the tunneled requests are
59
+ filtered for potential SSRF attacks.
60
+ - **Protocols Supported:** SOCKS4, SOCKS5, HTTP, and HTTPS proxy server protocols are supported.
61
+
62
+ .. note::
63
+
64
+ We rely on the ``requests`` and ``urllib3`` thus the list may change over time.
65
+
66
+ .. warning::
67
+
68
+ For SOCKS4 and SOCKS5, you need to run ``pip install requests[socks]``
69
+
70
+ Example Usage:
71
+
72
+ .. code-block:: python
73
+
74
+ from requests_hardened import Config, Manager
75
+
76
+ http_manager = Manager(
77
+ Config(
78
+ default_timeout=(2, 10),
79
+ never_redirect=False,
80
+ # Enable SSRF IP filter
81
+ ip_filter_enable=True,
82
+ ip_filter_allow_loopback_ips=False,
83
+ )
84
+ )
85
+
86
+ # List of proxies
87
+ proxies = {
88
+ "https": "socks5://127.0.0.1:8888",
89
+ "http": "socks5://127.0.0.1:8888",
90
+ }
91
+
92
+ # Sends the HTTP request using the proxy
93
+ resp = http_manager.send_request("GET", "https://example.com", proxies=proxies)
94
+ print(resp)
95
+
96
+
97
+ .. note::
98
+
99
+ For more details on using proxies with the ``requests`` library, see the `official
100
+ documentation <https://docs.python-requests.org/en/latest/user/advanced/#proxies>`_.
101
+
102
+
103
+ Full Example
104
+ ============
105
+
106
+ .. code-block:: python
107
+
108
+ from requests_hardened import Config, Manager
109
+
110
+ # Creates a global "manager" that can be used to create ``requests.Session``
111
+ # objects with hardening in place.
112
+ http_manager = Manager(
113
+ Config(
114
+ default_timeout=(2, 10),
115
+ never_redirect=False,
116
+ ip_filter_enable=True,
117
+ ip_filter_allow_loopback_ips=False,
118
+ user_agent_override=None
119
+ )
120
+ )
121
+
122
+ # Sends an HTTP request without re-using ``requests.Session``:
123
+ resp = http_manager.send_request("GET", "https://example.com")
124
+ print(resp)
125
+
126
+ # Sends HTTP requests with reusable ``requests.Session``:
127
+ with http_manager.get_session() as sess:
128
+ sess.request("GET", "https://example.com")
129
+ sess.request("POST", "https://example.com", json={"foo": "bar"})
130
+
131
+
132
+ .. _PyPI: https://pypi.org/project/requests-hardened
133
+
134
+ .. |pypi-latest-version| image:: https://img.shields.io/pypi/v/requests-hardened.svg
135
+ :alt: Latest Version
136
+ :target: `PyPI`_
137
+
138
+ .. |pypi-python-versions| image:: https://img.shields.io/pypi/pyversions/requests-hardened.svg
139
+ :alt: Supported Python Versions
140
+ :target: `PyPI`_
141
+
142
+ .. |pypi-implementations| image:: https://img.shields.io/pypi/implementation/requests-hardened.svg
143
+ :alt: Supported Implementations
144
+ :target: `PyPI`_
@@ -0,0 +1,82 @@
1
+ [build-system]
2
+ build-backend = "poetry.core.masonry.api"
3
+ requires = ["poetry-core>=1.5.0"]
4
+
5
+ [tool.poetry]
6
+ name = "requests-hardened"
7
+ readme = "README.rst"
8
+ description = "A library that overrides the default behaviors of the requests library, and adds new security features."
9
+ authors = [
10
+ "Saleor Commerce <hello@saleor.io>"
11
+ ]
12
+ license = "BSD-3-Clause"
13
+ version = "1.0.0"
14
+ classifiers = [
15
+ "Development Status :: 5 - Production/Stable",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: BSD License",
18
+ "Natural Language :: English",
19
+ "Topic :: Security",
20
+ "Operating System :: POSIX",
21
+ "Operating System :: POSIX :: BSD",
22
+ "Operating System :: POSIX :: Linux",
23
+ "Operating System :: MacOS :: MacOS X",
24
+ 'Operating System :: Microsoft :: Windows',
25
+ "Programming Language :: Python",
26
+ "Programming Language :: Python :: 3",
27
+ "Programming Language :: Python :: 3 :: Only",
28
+ "Programming Language :: Python :: 3.9",
29
+ "Programming Language :: Python :: 3.10",
30
+ "Programming Language :: Python :: 3.11",
31
+ "Programming Language :: Python :: 3.12",
32
+ "Programming Language :: Python :: 3.13",
33
+ "Programming Language :: Python :: Implementation :: CPython",
34
+ "Programming Language :: Python :: Implementation :: PyPy",
35
+ ]
36
+
37
+ [tool.poetry.urls]
38
+ Homepage = "https://github.com/saleor/requests-hardened/"
39
+ Source = "https://github.com/saleor/requests-hardened/"
40
+ Issues = "https://github.com/saleor/requests-hardened/issues"
41
+ Changelog = "https://github.com/saleor/requests-hardened/releases/"
42
+
43
+ [tool.poetry.dependencies]
44
+ python = ">=3.9,<4.0"
45
+ # We require >=2.32.3 due to depending on `get_connection_with_tls_context`.
46
+ requests = ">=2.32.3,<3.0.0"
47
+
48
+ [tool.poetry.group.dev.dependencies]
49
+ pytest = "^7.4.0"
50
+ mypy = "^1.5.1"
51
+ types-requests = ">=2.32.0,<3.0.0"
52
+ trustme = ">=1.1.0,<2.0.0"
53
+ pytest-socket = "^0.7.0"
54
+ pproxy = "^2.7.9"
55
+ requests = { version = ">=2.32.3,<3.0.0" , extras = ["socks"] }
56
+
57
+ [tool.setuptools]
58
+ zip-safe = false
59
+ packages = ["requests_hardened"]
60
+
61
+ [tool.pytest.ini_options]
62
+ # Disallow creating sockets (using 'pytest-socket' library)
63
+ # We block sockets during tests in order to detect unexpected
64
+ # leaks/connections being made.
65
+ addopts = "--disable-socket"
66
+
67
+ [tool.mypy]
68
+ # Checks
69
+ check_untyped_defs = true
70
+ ignore_missing_imports = true
71
+ allow_redefinition = true
72
+
73
+ # Error messages
74
+ pretty = true
75
+ show_column_numbers = true
76
+ show_error_codes = true
77
+ show_error_context = true
78
+ show_traceback = true
79
+
80
+ exclude = [
81
+ "tests/"
82
+ ]
@@ -0,0 +1,3 @@
1
+ from requests_hardened.client import HTTPSession
2
+ from requests_hardened.config import Config
3
+ from requests_hardened.manager import Manager
@@ -0,0 +1,51 @@
1
+ from typing import Union, Any
2
+
3
+ import requests
4
+ from requests import Request, PreparedRequest, Response
5
+
6
+ from requests_hardened.config import Config
7
+ from requests_hardened.ip_filter_adapter import IPFilterAdapter
8
+
9
+ T_TIMEOUT = Union[int, float]
10
+
11
+
12
+ class HTTPSession(requests.Session):
13
+ def __init__(self, config: Config, **kwargs):
14
+ super().__init__(**kwargs)
15
+
16
+ if config.ip_filter_enable is True:
17
+ self.mount(
18
+ "http://",
19
+ IPFilterAdapter(
20
+ is_https_proto=False, # We use http:// (insecure)
21
+ allow_loopback=config.ip_filter_allow_loopback_ips,
22
+ tls_sni_support=config.ip_filter_tls_sni_support,
23
+ ),
24
+ )
25
+ self.mount(
26
+ "https://",
27
+ IPFilterAdapter(
28
+ is_https_proto=True, # We use https:// (SSL/TLS)
29
+ allow_loopback=config.ip_filter_allow_loopback_ips,
30
+ tls_sni_support=config.ip_filter_tls_sni_support,
31
+ ),
32
+ )
33
+
34
+ self._config = config
35
+
36
+ def send(self, request: PreparedRequest, **kwargs: Any) -> Response:
37
+ allow_redirects = kwargs.setdefault(
38
+ "allow_redirects", not self._config.never_redirect
39
+ )
40
+ if allow_redirects and self._config.never_redirect:
41
+ kwargs["allow_redirects"] = False
42
+
43
+ timeout = kwargs.setdefault("timeout", self._config.default_timeout)
44
+ if not timeout:
45
+ kwargs["timeout"] = self._config.default_timeout
46
+ return super().send(request, **kwargs)
47
+
48
+ def prepare_request(self, request: Request) -> PreparedRequest:
49
+ if self._config.user_agent_override is not None:
50
+ request.headers.update({"User-Agent": self._config.user_agent_override})
51
+ return super().prepare_request(request)
@@ -0,0 +1,27 @@
1
+ import dataclasses
2
+ from typing import Optional
3
+
4
+ from requests_hardened.types import T_TIMEOUT_TUPLE
5
+
6
+
7
+ @dataclasses.dataclass
8
+ class Config:
9
+ # If ``True``, then any private and loopback IP will be rejected.
10
+ ip_filter_enable: bool
11
+
12
+ # If ``True``, then loopback IPs are allowed
13
+ # when ``ip_filter_enable`` is set to ``True``.
14
+ ip_filter_allow_loopback_ips: bool
15
+
16
+ # If True, then HTTP redirects are never allowed.
17
+ never_redirect: bool
18
+
19
+ # The default timeout value to set to all requests.
20
+ default_timeout: Optional[T_TIMEOUT_TUPLE]
21
+
22
+ # Override the default User-Agent header of the requests library.
23
+ user_agent_override: Optional[str] = None
24
+
25
+ # Whether to enable support for TLS SNI for IP filtering.
26
+ # Do not disable this option unless you are absolutely sure of what you are doing!
27
+ ip_filter_tls_sni_support: bool = True
@@ -0,0 +1,80 @@
1
+ import ipaddress
2
+ import logging
3
+ import socket
4
+ from typing import Tuple, Union
5
+
6
+ import requests.exceptions
7
+ from urllib3.util.connection import ( # type: ignore[attr-defined] # `allowed_gai_family` exists. # noqa: E501
8
+ allowed_gai_family,
9
+ )
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class InvalidIPAddress(requests.RequestException):
15
+ pass
16
+
17
+
18
+ def get_ip_address(
19
+ hostname: str, port: int, allow_loopback: bool
20
+ ) -> Tuple[Union[ipaddress.IPv4Address, ipaddress.IPv6Address], int]:
21
+ # Development safeguard, hostname shouldn't be surrounded by brackets
22
+ # when calling this function.
23
+ assert not hostname.startswith("[")
24
+
25
+ addresses = socket.getaddrinfo(
26
+ hostname, port, family=allowed_gai_family(), type=socket.SOCK_STREAM
27
+ )
28
+
29
+ af, socktype, proto, _canonname, socket_address = addresses[0]
30
+
31
+ if af != socket.AF_INET and af != socket.AF_INET6:
32
+ # This code shouldn't be reachable.
33
+ raise ValueError(
34
+ "Only AF_INET and AF_INET6 socket address families are supported"
35
+ )
36
+ port = socket_address[1]
37
+ ip = ipaddress.ip_address(socket_address[0])
38
+
39
+ # Python considers special IP ranges representing IPv4 addresses as being
40
+ # private ranges, such as ::ffff:0:0/96, ::/128.
41
+ # Because of that, we need to check the IPv4 address instead of the IPv6 one.
42
+ # https://github.com/python/cpython/commit/ed391090cc8332406e6225d40877db6ff44a7104
43
+ if ip.version == 6 and (ipv4 := ip.ipv4_mapped) is not None:
44
+ ip = ipv4
45
+
46
+ if allow_loopback and ip.is_loopback:
47
+ return ip, port
48
+ elif ip.is_private:
49
+ logger.warning(
50
+ "Forbidden IP address: %s for hostname %s",
51
+ ip,
52
+ hostname,
53
+ # Extra arguments may clash with logger configuration
54
+ # if it injects extra JSON fields, such as:
55
+ # https://github.com/saleor/saleor/blob/5e7c57dad9e64b09477ebdcee53f0277359bc598/saleor/core/logging.py#L13
56
+ # Because of that, we prefix the extra arguments in order to
57
+ # reduce the chance of clashing with logging configs.
58
+ extra={"dst_ip": ip, "dst_hostname": hostname},
59
+ )
60
+ raise InvalidIPAddress(ip)
61
+ return ip, port
62
+
63
+
64
+ def filter_host(
65
+ hostname: str, port: int, *, allow_loopback: bool
66
+ ) -> str:
67
+ if not hostname:
68
+ raise requests.exceptions.InvalidURL("Invalid URL: missing hostname")
69
+
70
+ try:
71
+ ip_addr, port = get_ip_address(
72
+ hostname, port, allow_loopback=allow_loopback
73
+ )
74
+ except socket.gaierror as exc:
75
+ raise requests.ConnectionError("Failed to resolve domain") from exc
76
+ except socket.timeout as exc:
77
+ raise requests.ConnectTimeout("Failed to connect to host") from exc
78
+
79
+ ip_addr_str = str(ip_addr) if ip_addr.version != 6 else f"[{ip_addr}]"
80
+ return ip_addr_str
@@ -0,0 +1,76 @@
1
+ from requests.adapters import HTTPAdapter
2
+ from requests_hardened.ip_filter import filter_host
3
+
4
+
5
+ class IPFilterAdapter(HTTPAdapter):
6
+ def __init__(
7
+ self,
8
+ is_https_proto: bool,
9
+ allow_loopback: bool = False,
10
+ tls_sni_support: bool = True,
11
+ ):
12
+ """
13
+ :param is_https_proto: whether it is HTTPS or insecure HTTP.
14
+ :param allow_loopback: whether to allow loopback IP addresses in IP filtering.
15
+ :param tls_sni_support: whether to add support for TLS SNI (enabled by default,
16
+ and shouldn't be changed unless you are certain).
17
+ """
18
+ super().__init__()
19
+ self._is_https_proto = is_https_proto
20
+ self._allow_loopback = allow_loopback
21
+ self._tls_sni_support = tls_sni_support
22
+
23
+ def build_connection_pool_key_attributes(self, request, verify, cert=None):
24
+ host_params, pool_kwargs = super().build_connection_pool_key_attributes(
25
+ request, verify, cert
26
+ )
27
+
28
+ # Copy headers before mutating them as they may be a global variable used by
29
+ # subsequent requests.
30
+ # e.g., https://github.com/lepture/authlib/blob/a7d68b4c3b8a3a7fe0b62943b5228669f2f3dfec/authlib/oauth2/client.py#L205-L206
31
+ if request.headers:
32
+ request.headers = dict(**request.headers)
33
+ else:
34
+ request.headers = {}
35
+
36
+ # Adds the original URL hostname as the 'Host' header.
37
+ original_host = request.headers["Host"] = host_params["host"]
38
+
39
+ # Set the original hostname for certificate validation otherwise
40
+ # urllib3 will try to match the pinned resolved IP address against the
41
+ # certificate.
42
+ #
43
+ # Note: assert_hostname and server_hostname cannot be passed
44
+ # when using insecure HTTP (http://) as it's not a valid argument for
45
+ # `urllib3.connectionpool.HTTPConnectionPool`.
46
+ if self._is_https_proto is True:
47
+ # For non-TLS SNI servers.
48
+ pool_kwargs["assert_hostname"] = original_host # type: ignore[typeddict-unknown-key] # Valid parameter for urllib3.connectionpool.HTTPSConnectionPool
49
+
50
+ # Support TLS servers with SNI callbacks.
51
+ if self._tls_sni_support is True:
52
+ pool_kwargs["server_hostname"] = original_host # type: ignore[typeddict-unknown-key] # Valid parameter for urllib3.connectionpool.HTTPSConnectionPool
53
+
54
+ # Override the connection hostname to the resolved IP address,
55
+ # and reject if it's a private IP.
56
+ host_params["host"] = filter_host(
57
+ original_host,
58
+ host_params["port"],
59
+ allow_loopback=self._allow_loopback,
60
+ )
61
+ return host_params, pool_kwargs
62
+
63
+ def get_connection(self, url, proxies=None):
64
+ # Note: we do not support this method due to being deprecated since May 2024.
65
+ # Only `get_connection_with_tls_context` is supported by our package,
66
+ # due to the deprecated `get_connection` being largely different
67
+ # and is unlikely to still be used by other packages.
68
+ # Additional references:
69
+ # - https://github.com/psf/requests/pull/6655
70
+ # - https://github.com/psf/requests/pull/6710
71
+ # - https://github.com/advisories/GHSA-9wx4-h78v-vm56
72
+ raise NotImplementedError(
73
+ "get_connection is not supported in requests-hardened>=v1.0.0b5\n"
74
+ "Upgrade your 'requests' package to >=2.32.2 and your dependencies "
75
+ "if they rely on `requests.adapters.HTTPAdapter.get_connection`."
76
+ )
@@ -0,0 +1,21 @@
1
+ from copy import copy
2
+
3
+ from requests_hardened import HTTPSession
4
+ from requests_hardened.config import Config
5
+
6
+
7
+ class Manager:
8
+ __slots__ = ("config",)
9
+
10
+ def __init__(self, config: Config):
11
+ self.config = config
12
+
13
+ def clone(self):
14
+ return Manager(config=copy(self.config))
15
+
16
+ def get_session(self):
17
+ return HTTPSession(self.config)
18
+
19
+ def send_request(self, method: str, url: str, **kwargs):
20
+ with self.get_session() as sess:
21
+ return sess.request(method, url, **kwargs)
@@ -0,0 +1,5 @@
1
+ from typing import Union, Tuple
2
+
3
+ T_TIMEOUT = Union[int, float]
4
+ T_TIMEOUT_TUPLE = Tuple[T_TIMEOUT, T_TIMEOUT]
5
+ T_REQUESTS_TIMEOUT_ARG = Union[T_TIMEOUT, Tuple[T_TIMEOUT, T_TIMEOUT]]