From b084460eb7d58446a99a69fc209566370ba6e006 Mon Sep 17 00:00:00 2001 From: Aditya Singh Date: Fri, 8 May 2026 04:13:58 -0700 Subject: [PATCH] fix(sandbox): require https for vercel exposed port domains MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit VercelSandboxSession._resolve_exposed_port accepted any URL returned by sandbox.domain() — including http:// or schemeless — and silently exposed it as a non-TLS endpoint (tls=False, port 80). Vercel sandbox preview domains are always served over HTTPS, so a non-https scheme indicates either a backend bug or a tampered response. Either way, downstream code would then connect over plaintext to a host derived from that URL. Reject any domain whose scheme is not https with the existing ExposedPortUnavailableError path, and drop the conditional http fallback for tls/port. Mirrors the same defensive narrowing applied to the daytona backend in #3206. --- .../extensions/sandbox/vercel/sandbox.py | 7 ++--- tests/extensions/sandbox/test_vercel.py | 29 ++++++++++++++++++- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/agents/extensions/sandbox/vercel/sandbox.py b/src/agents/extensions/sandbox/vercel/sandbox.py index 92513077ab..c87438cb73 100644 --- a/src/agents/extensions/sandbox/vercel/sandbox.py +++ b/src/agents/extensions/sandbox/vercel/sandbox.py @@ -460,18 +460,17 @@ async def _resolve_exposed_port(self, port: int) -> ExposedPortEndpoint: parsed = urlsplit(domain) host = parsed.hostname - if not host: + if not host or parsed.scheme != "https": raise ExposedPortUnavailableError( port=port, exposed_ports=self.state.exposed_ports, reason="backend_unavailable", context={"backend": "vercel", "domain": domain}, ) - tls = parsed.scheme == "https" return ExposedPortEndpoint( host=host, - port=parsed.port or (443 if tls else 80), - tls=tls, + port=parsed.port or 443, + tls=True, ) async def read(self, path: Path, *, user: str | User | None = None) -> io.IOBase: diff --git a/tests/extensions/sandbox/test_vercel.py b/tests/extensions/sandbox/test_vercel.py index 306acf9527..1941ba1ed3 100644 --- a/tests/extensions/sandbox/test_vercel.py +++ b/tests/extensions/sandbox/test_vercel.py @@ -16,7 +16,11 @@ from agents.sandbox import Manifest, SandboxPathGrant from agents.sandbox.entries import File, InContainerMountStrategy, Mount, MountpointMountPattern from agents.sandbox.entries.mounts.base import InContainerMountAdapter -from agents.sandbox.errors import ConfigurationError, InvalidManifestPathError +from agents.sandbox.errors import ( + ConfigurationError, + ExposedPortUnavailableError, + InvalidManifestPathError, +) from agents.sandbox.manifest import Environment from agents.sandbox.materialization import MaterializedFile from agents.sandbox.session.base_sandbox_session import BaseSandboxSession @@ -541,6 +545,29 @@ async def test_vercel_exec_read_write_and_port_resolution(monkeypatch: pytest.Mo assert payload.read() == b"payload" +@pytest.mark.asyncio +async def test_vercel_resolve_exposed_port_rejects_non_https_domain( + monkeypatch: pytest.MonkeyPatch, +) -> None: + vercel_module = _load_vercel_module(monkeypatch) + + state = vercel_module.VercelSandboxSessionState( + session_id="00000000-0000-0000-0000-000000000099", + manifest=Manifest(), + snapshot=NoopSnapshot(id="snapshot"), + sandbox_id="sandbox-http", + exposed_ports=(3000,), + ) + sandbox = _FakeAsyncSandbox( + sandbox_id="sandbox-http", + routes=[{"port": 3000, "url": "http://3000-sandbox.vercel.run"}], + ) + session = vercel_module.VercelSandboxSession.from_state(state, sandbox=sandbox) + + with pytest.raises(ExposedPortUnavailableError): + await session.resolve_exposed_port(3000) + + @pytest.mark.asyncio async def test_vercel_start_uses_base_session_contract_and_materializes_workspace( monkeypatch: pytest.MonkeyPatch,