Problem
Every caller-URL entry point into the Playwright browser container checks the URL once, in the Python process (is_public_url_async: send_to_browser_vm, and capture_screenshot after #16544). The navigation itself runs in autobot-browser-worker/playwright-server.js (page.goto(...)), which resolves DNS again and follows redirects with no re-check. A public URL that 302s to an internal address, or a hostname that rebinds between check and fetch, reaches internal services from the container.
The other fetch paths already close this: research_browser_manager.py re-checks after navigation (#13018), and content_reach/web_fetch pin the resolved IP through redirects (#13017, #13019). The container path (services/playwright_service.py, browser_backends/container.py, autobot_shared/browser/registry.py::_guard_url) has neither. Found in #16544's review. The separately unguarded POST /playwright/navigate is tracked in #16552.
Acceptance criteria
Problem
Every caller-URL entry point into the Playwright browser container checks the URL once, in the Python process (
is_public_url_async:send_to_browser_vm, andcapture_screenshotafter #16544). The navigation itself runs inautobot-browser-worker/playwright-server.js(page.goto(...)), which resolves DNS again and follows redirects with no re-check. A public URL that 302s to an internal address, or a hostname that rebinds between check and fetch, reaches internal services from the container.The other fetch paths already close this:
research_browser_manager.pyre-checks after navigation (#13018), andcontent_reach/web_fetchpin the resolved IP through redirects (#13017, #13019). The container path (services/playwright_service.py,browser_backends/container.py,autobot_shared/browser/registry.py::_guard_url) has neither. Found in #16544's review. The separately unguardedPOST /playwright/navigateis tracked in #16552.Acceptance criteria
url_safety.