Skip to content

Commit fac07a7

Browse files
authored
Merge pull request #24 from proxymesh/cursor/vulnerability-findings-memory-4cec
Harden CONNECT header handling against injection and spoofing
2 parents a79847c + e4acd84 commit fac07a7

17 files changed

Lines changed: 602 additions & 30 deletions

‎.github/dependabot.yml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,7 @@ updates:
1111
directory: "/"
1212
schedule:
1313
interval: "weekly"
14+
- package-ecosystem: "github-actions"
15+
directory: "/"
16+
schedule:
17+
interval: "weekly"

‎.github/workflows/github_release_on_release_branch_merge.yml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,12 @@ jobs:
2626
runs-on: ubuntu-latest
2727
steps:
2828
- name: Checkout merge commit
29-
uses: actions/checkout@v6
29+
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803
3030
with:
3131
ref: ${{ github.event.pull_request.merge_commit_sha }}
3232

3333
- name: Set up Python
34-
uses: actions/setup-python@v6
34+
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
3535
with:
3636
python-version: "3.12"
3737

‎.github/workflows/proxy_integration_tests.yml‎

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,40 @@ permissions:
1212
contents: read
1313

1414
jobs:
15+
unit:
16+
runs-on: ubuntu-latest
17+
steps:
18+
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803
19+
with:
20+
persist-credentials: false
21+
22+
- name: Set up Python
23+
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
24+
with:
25+
python-version: "3.x"
26+
27+
- name: Install system dependencies (pycurl)
28+
run: sudo apt-get update && sudo apt-get install -y libcurl4-openssl-dev
29+
30+
- name: Install package and test dependencies
31+
run: |
32+
python -m pip install --upgrade pip
33+
pip install -e .
34+
pip install urllib3 requests aiohttp httpx pycurl
35+
36+
- name: Run header security unit tests
37+
run: python test_header_security.py
38+
1539
integration:
1640
runs-on: ubuntu-latest
1741

1842
steps:
19-
- uses: actions/checkout@v6
43+
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803
2044
with:
2145
persist-credentials: false
2246

2347
- name: Set up Python
24-
uses: actions/setup-python@v6
48+
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
2549
with:
2650
python-version: "3.x"
2751

‎.github/workflows/publish.yml‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ jobs:
3030
outputs:
3131
publish: ${{ steps.decide.outputs.publish }}
3232
steps:
33-
- uses: actions/checkout@v6
33+
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803
3434
if: github.event_name == 'workflow_run'
3535
with:
3636
ref: main
@@ -64,12 +64,12 @@ jobs:
6464
runs-on: ubuntu-latest
6565

6666
steps:
67-
- uses: actions/checkout@v6
67+
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803
6868
with:
6969
ref: ${{ github.event_name == 'workflow_run' && 'main' || github.event_name == 'release' && github.ref || 'main' }}
7070

7171
- name: Set up Python
72-
uses: actions/setup-python@v6
72+
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
7373
with:
7474
python-version: "3.x"
7575

@@ -82,7 +82,7 @@ jobs:
8282
run: python -m build
8383

8484
- name: Store distribution packages
85-
uses: actions/upload-artifact@v7
85+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
8686
with:
8787
name: python-package-distributions
8888
path: dist/
@@ -102,13 +102,13 @@ jobs:
102102

103103
steps:
104104
- name: Download distribution packages
105-
uses: actions/download-artifact@v8
105+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
106106
with:
107107
name: python-package-distributions
108108
path: dist/
109109

110110
- name: Publish to PyPI
111-
uses: pypa/gh-action-pypi-publish@release/v1
111+
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33
112112

113113
publish-to-testpypi:
114114
name: Publish to TestPyPI
@@ -122,12 +122,12 @@ jobs:
122122

123123
steps:
124124
- name: Download distribution packages
125-
uses: actions/download-artifact@v8
125+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
126126
with:
127127
name: python-package-distributions
128128
path: dist/
129129

130130
- name: Publish to TestPyPI
131-
uses: pypa/gh-action-pypi-publish@release/v1
131+
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33
132132
with:
133133
repository-url: https://test.pypi.org/legacy/

‎docs/aiohttp.rst‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,7 @@ When establishing an HTTPS connection through a proxy, the connector:
284284
* Creates a CONNECT request to the proxy server
285285
* Includes any custom proxy headers you've specified
286286
* Captures the proxy's response headers (e.g., ``X-ProxyMesh-IP``)
287-
* Stores them so they can be merged into the final response
287+
* Stores them so safe CONNECT values can be merged into the final response without overwriting origin headers. Hop-by-hop and security-sensitive names are omitted. The full CONNECT set is on ``response.proxy_headers``.
288288

289289
You typically don't need to use this class directly - it's automatically configured when using ``ProxyClientSession``.
290290

‎docs/httpx.rst‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -375,7 +375,7 @@ The extension classes work by intercepting the CONNECT request/response cycle du
375375

376376
2. **CONNECT Response**: The proxy responds with a CONNECT response (status 200) that may include proxy information headers in the response (e.g., ``X-ProxyMesh-IP: 192.168.1.1``)
377377

378-
3. **Header Merging**: These proxy response headers are captured during the tunnel establishment and stored. When the actual HTTP request is made through the tunnel, the proxy response headers are merged into the final HTTP response headers using ``merge_headers()``
378+
3. **Header Merging**: Safe CONNECT headers are stored and later merged into the origin response with ``merge_headers()``. Origin headers are never overwritten, and hop-by-hop or security-sensitive names such as ``Set-Cookie`` or ``Location`` from CONNECT are not copied.
379379

380380
4. **Access**: Your application can then access both the target server's response headers and the proxy's response headers from the same response object
381381

‎docs/requests.rst‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ This section shows you how to quickly get up and running with proxy headers in r
4646
4747
That's it! The ``requests_adapter`` module handles sending your custom headers to the proxy and makes proxy response headers available in the response.
4848

49+
CONNECT response headers are copied onto ``response.headers`` except hop-by-hop and security-sensitive names (``Set-Cookie``, ``Location``, ``Content-Type``, and similar). Existing origin headers are never overwritten. The full CONNECT set is on ``response.proxy_headers``.
50+
4951
Using Proxies with requests
5052
---------------------------
5153

‎docs/urllib3.rst‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ This section shows you how to quickly get up and running with proxy headers in u
4545
4646
That's it! The ``ProxyHeaderManager`` handles sending your custom headers to the proxy and makes proxy response headers available in the response.
4747

48+
CONNECT response headers are copied onto ``response.headers`` except hop-by-hop and security-sensitive names (``Set-Cookie``, ``Location``, ``Content-Type``, and similar). Existing origin headers are never overwritten. The full CONNECT set is on ``response.proxy_headers``.
49+
4850
Using Proxies with urllib3
4951
--------------------------
5052

‎python_proxy_headers/aiohttp_proxy.py‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from aiohttp.helpers import reify
77
from aiohttp import hdrs
88
from multidict import CIMultiDict, CIMultiDictProxy
9+
from .header_utils import merge_proxy_response_headers
910

1011
class ProxyTCPConnector(TCPConnector):
1112
async def _create_proxy_connection(self, req: ClientRequest, traces, timeout):
@@ -143,13 +144,21 @@ async def send(self, conn):
143144
return resp
144145

145146
class ProxyClientResponse(ClientResponse):
147+
@reify
148+
def proxy_headers(self):
149+
"""Headers from the proxy CONNECT response, not merged into origin headers."""
150+
raw = getattr(self, "_proxy_headers", None)
151+
if raw:
152+
return CIMultiDictProxy(CIMultiDict(raw))
153+
return CIMultiDictProxy(CIMultiDict())
154+
146155
@reify
147156
def headers(self):
148157
proxy_headers = getattr(self, '_proxy_headers', None)
149158

150159
if proxy_headers:
151160
headers = CIMultiDict(self._headers)
152-
headers.extend(proxy_headers)
161+
merge_proxy_response_headers(headers, proxy_headers)
153162
return CIMultiDictProxy(headers)
154163
else:
155164
return self._headers

‎python_proxy_headers/autoscraper_proxy.py‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"Install it with: pip install autoscraper"
3636
)
3737

38+
from .header_utils import validate_headers
3839
from .requests_adapter import ProxySession
3940

4041

@@ -71,7 +72,7 @@ def __init__(
7172
stack_list: Optional[List] = None
7273
):
7374
super().__init__(stack_list=stack_list)
74-
self._proxy_headers = proxy_headers or {}
75+
self._proxy_headers = validate_headers(proxy_headers)
7576
self._session: Optional[ProxySession] = None
7677

7778
def _get_session(self) -> ProxySession:
@@ -90,7 +91,7 @@ def set_proxy_headers(self, proxy_headers: Dict[str, str]):
9091
Args:
9192
proxy_headers: New proxy headers to use
9293
"""
93-
self._proxy_headers = proxy_headers
94+
self._proxy_headers = validate_headers(proxy_headers)
9495
if self._session is not None:
9596
self._session.close()
9697
self._session = None

0 commit comments

Comments
 (0)