fix: validate Playwright navigations and gate redirects in web loader (#24756)
SafePlaywrightURLLoader validated only the initially submitted URL and then let the browser follow HTTP redirects and client-side navigations without re-checking them, so a public URL could redirect into the internal network (cloud metadata, RFC1918, loopback). Intercept document-type requests, re-run validate_url on each, and apply the same redirect policy as the requests loader (blocked unless AIOHTTP_CLIENT_ALLOW_REDIRECTS). Sub-resource requests pass through unchanged so page rendering performance is unaffected. Co-authored-by: POV9en <POV9en@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 file changed
+58
@@ -421,6 +421,62 @@ class SafePlaywrightURLLoader(PlaywrightURLLoader, RateLimitMixin, URLProcessing
|
|||||||
self.trust_env = trust_env
|
self.trust_env = trust_env
|
||||||
self.playwright_timeout = playwright_timeout
|
self.playwright_timeout = playwright_timeout
|
||||||
|
|
||||||
|
def _intercept_navigation_sync(self, route, request=None):
|
||||||
|
req = request or route.request
|
||||||
|
|
||||||
|
if req.resource_type != 'document':
|
||||||
|
route.continue_()
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
validate_url(req.url)
|
||||||
|
except Exception:
|
||||||
|
route.abort()
|
||||||
|
return
|
||||||
|
|
||||||
|
if AIOHTTP_CLIENT_ALLOW_REDIRECTS:
|
||||||
|
resp = route.fetch()
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
resp = route.fetch(max_redirects=0)
|
||||||
|
except TypeError:
|
||||||
|
route.abort()
|
||||||
|
return
|
||||||
|
|
||||||
|
if 300 <= resp.status < 400:
|
||||||
|
route.abort()
|
||||||
|
return
|
||||||
|
|
||||||
|
route.fulfill(response=resp)
|
||||||
|
|
||||||
|
async def _intercept_navigation(self, route, request=None):
|
||||||
|
req = request or route.request
|
||||||
|
|
||||||
|
if req.resource_type != 'document':
|
||||||
|
await route.continue_()
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
await run_in_threadpool(validate_url, req.url)
|
||||||
|
except Exception:
|
||||||
|
await route.abort()
|
||||||
|
return
|
||||||
|
|
||||||
|
if AIOHTTP_CLIENT_ALLOW_REDIRECTS:
|
||||||
|
resp = await route.fetch()
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
resp = await route.fetch(max_redirects=0)
|
||||||
|
except TypeError:
|
||||||
|
await route.abort()
|
||||||
|
return
|
||||||
|
|
||||||
|
if 300 <= resp.status < 400:
|
||||||
|
await route.abort()
|
||||||
|
return
|
||||||
|
|
||||||
|
await route.fulfill(response=resp)
|
||||||
|
|
||||||
def lazy_load(self) -> Iterator[Document]:
|
def lazy_load(self) -> Iterator[Document]:
|
||||||
"""Safely load URLs synchronously with support for remote browser."""
|
"""Safely load URLs synchronously with support for remote browser."""
|
||||||
from playwright.sync_api import sync_playwright
|
from playwright.sync_api import sync_playwright
|
||||||
@@ -436,6 +492,7 @@ class SafePlaywrightURLLoader(PlaywrightURLLoader, RateLimitMixin, URLProcessing
|
|||||||
try:
|
try:
|
||||||
self._safe_process_url_sync(url)
|
self._safe_process_url_sync(url)
|
||||||
page = browser.new_page()
|
page = browser.new_page()
|
||||||
|
page.route('**/*', self._intercept_navigation_sync)
|
||||||
response = page.goto(url, timeout=self.playwright_timeout)
|
response = page.goto(url, timeout=self.playwright_timeout)
|
||||||
if response is None:
|
if response is None:
|
||||||
raise ValueError(f'page.goto() returned None for url {url}')
|
raise ValueError(f'page.goto() returned None for url {url}')
|
||||||
@@ -465,6 +522,7 @@ class SafePlaywrightURLLoader(PlaywrightURLLoader, RateLimitMixin, URLProcessing
|
|||||||
try:
|
try:
|
||||||
await self._safe_process_url(url)
|
await self._safe_process_url(url)
|
||||||
page = await browser.new_page()
|
page = await browser.new_page()
|
||||||
|
await page.route('**/*', self._intercept_navigation)
|
||||||
response = await page.goto(url, timeout=self.playwright_timeout)
|
response = await page.goto(url, timeout=self.playwright_timeout)
|
||||||
if response is None:
|
if response is None:
|
||||||
raise ValueError(f'page.goto() returned None for url {url}')
|
raise ValueError(f'page.goto() returned None for url {url}')
|
||||||
|
|||||||
Reference in new issue
Block a user