Compare commits
4 commits
main
...
fix/web-se
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2fc61ebab3 |
||
|
|
ed836f143f | ||
|
|
3182fd953c | ||
|
|
bf55e0cba4 |
2 changed files with 81 additions and 11 deletions
|
|
@ -215,9 +215,17 @@ def _fetch_page_text(
|
|||
return reason
|
||||
|
||||
try:
|
||||
import http.client
|
||||
import socket
|
||||
import ssl
|
||||
import urllib.request
|
||||
from urllib.error import HTTPError as _HTTPError
|
||||
from urllib.parse import urljoin, urlunparse
|
||||
from urllib.parse import urljoin
|
||||
|
||||
try:
|
||||
import certifi
|
||||
except ImportError:
|
||||
certifi = None
|
||||
|
||||
# Disable auto-redirect so we can validate each hop for SSRF.
|
||||
# urllib raises HTTPError for 3xx when the handler returns None,
|
||||
|
|
@ -226,23 +234,85 @@ def _fetch_page_text(
|
|||
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
||||
return None
|
||||
|
||||
opener = urllib.request.build_opener(_NoRedirect)
|
||||
class _PinnedHTTPSConnection(http.client.HTTPSConnection):
|
||||
"""HTTPS connection that pins to a pre-validated IP while
|
||||
preserving the original hostname for SNI and cert verification.
|
||||
Overrides ``_create_connection`` so that standard ``connect()``
|
||||
logic (TLS, proxy CONNECT tunneling, TCP_NODELAY) is preserved."""
|
||||
|
||||
_pinned_ip: str | None = None
|
||||
|
||||
def _create_connection(self, address, timeout, source_address):
|
||||
return socket.create_connection(
|
||||
(self._pinned_ip or address[0], address[1]),
|
||||
timeout,
|
||||
source_address,
|
||||
)
|
||||
|
||||
class _PinnedHTTPSHandler(urllib.request.HTTPSHandler):
|
||||
"""HTTPSHandler that routes connections through a pinned IP."""
|
||||
|
||||
def __init__(self, pinned_ip: str, context = None):
|
||||
super().__init__(context = context)
|
||||
self._pinned_ip = pinned_ip
|
||||
|
||||
def https_open(self, req):
|
||||
return self.do_open(self._make_connection, req, context = self._context)
|
||||
|
||||
def _make_connection(self, host, **kwargs):
|
||||
conn = _PinnedHTTPSConnection(host, **kwargs)
|
||||
conn._pinned_ip = self._pinned_ip
|
||||
return conn
|
||||
|
||||
class _PinnedHTTPConnection(http.client.HTTPConnection):
|
||||
"""HTTP connection that pins to a pre-validated IP."""
|
||||
|
||||
_pinned_ip: str | None = None
|
||||
|
||||
def _create_connection(self, address, timeout, source_address):
|
||||
return socket.create_connection(
|
||||
(self._pinned_ip or address[0], address[1]),
|
||||
timeout,
|
||||
source_address,
|
||||
)
|
||||
|
||||
class _PinnedHTTPHandler(urllib.request.HTTPHandler):
|
||||
"""HTTPHandler that routes connections through a pinned IP."""
|
||||
|
||||
def __init__(self, pinned_ip: str):
|
||||
super().__init__()
|
||||
self._pinned_ip = pinned_ip
|
||||
|
||||
def http_open(self, req):
|
||||
return self.do_open(self._make_connection, req)
|
||||
|
||||
def _make_connection(self, host, **kwargs):
|
||||
conn = _PinnedHTTPConnection(host, **kwargs)
|
||||
conn._pinned_ip = self._pinned_ip
|
||||
return conn
|
||||
|
||||
max_bytes = max_chars * 4 + 1
|
||||
current_url = url
|
||||
current_host = parsed.hostname
|
||||
|
||||
# Create SSL context once and reuse across redirect hops.
|
||||
ssl_ctx = ssl.create_default_context(
|
||||
cafile = certifi.where() if certifi else None,
|
||||
)
|
||||
ssl_ctx.minimum_version = ssl.TLSVersion.TLSv1_2
|
||||
|
||||
for _hop in range(5):
|
||||
# Pin to the validated IP to prevent DNS rebinding.
|
||||
# Rewrite the URL to use the IP and set the Host header.
|
||||
cp = urlparse(current_url)
|
||||
ip_netloc = f"{pinned_ip}:{cp.port}" if cp.port else pinned_ip
|
||||
pinned_url = urlunparse(cp._replace(netloc = ip_netloc))
|
||||
# Build opener with IP-pinning handler to prevent DNS rebinding.
|
||||
# The original hostname is preserved in the URL for correct SNI.
|
||||
if urlparse(current_url).scheme == "https":
|
||||
pin_handler = _PinnedHTTPSHandler(pinned_ip, context = ssl_ctx)
|
||||
else:
|
||||
pin_handler = _PinnedHTTPHandler(pinned_ip)
|
||||
opener = urllib.request.build_opener(_NoRedirect, pin_handler)
|
||||
|
||||
req = urllib.request.Request(
|
||||
pinned_url,
|
||||
current_url,
|
||||
headers = {
|
||||
"User-Agent": "UnslothStudio/1.0",
|
||||
"Host": current_host,
|
||||
},
|
||||
)
|
||||
try:
|
||||
|
|
@ -266,7 +336,6 @@ def _fetch_page_text(
|
|||
)
|
||||
if not ok2:
|
||||
return reason2
|
||||
current_host = rp.hostname
|
||||
continue
|
||||
# Success -- read capped body
|
||||
raw_bytes = resp.read(max_bytes)
|
||||
|
|
|
|||
|
|
@ -15,3 +15,4 @@ huggingface-hub==0.36.2
|
|||
structlog>=24.1.0
|
||||
diceware
|
||||
ddgs
|
||||
certifi
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue