Skip to content

fix: harden HFSandboxProvider local auth proxy - #1558

Merged
burtenshaw merged 1 commit into
openenv:mainfrom
dongjiang1989:fix/hf-proxy-hardening
Nov 5, 2026
Merged

burtenshaw merged 1 commit into
openenv:mainfrom
dongjiang1989:fix/hf-proxy-hardening

Conversation

@dongjiang1989

@dongjiang1989 dongjiang1989 commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Hardens _LocalAuthProxy in hf_sandbox_provider.py, split out of the review discussion in #1556 (finding: the same proxy code is copied there, and the issues apply to code already on main).

  • Reject upstream redirects instead of following them: HTTP uses allow_redirects=False (3xx becomes a generic 502); WebSocket handshakes disable websockets' redirect following via process_redirect. Following a redirect replays the injected auth headers (and POST bodies) to another origin and can downgrade https to http. Verified against websockets 15.0.1 and 16.0: the default connector does replay additional_headers cross-origin.
  • Sanitize WebSocket handshake failures: caught inside the route, logged as a generic warning, client gets a plain denial. The upstream exception chain can embed credentials in the target URL (.claude/docs/INVARIANTS.md credential rule).
  • Retain proxy handles on a stuck shutdown: stop() sets force_exit to bound the drain and raises RuntimeError if the thread survives the join, instead of dropping the handles and leaking a bound loopback port. stop_container() still kills the sandbox first and re-raises after.
  • Refuse restart while a proxy awaits cleanup: the start guard now also checks _proxy, so a retained (possibly still bound) proxy cannot be overwritten and lost.

Adds tests/test_core/test_hf_sandbox_provider.py — a dedicated regression suite for the provider and proxy (faked pool + loopback fake upstream, no HF account or network): header injection over HTTP/WS, HTTP 307 and cross-origin WS 302 rejection with an attacker recorder, handshake log sanitization, stuck-thread retention, kill-despite-proxy-failure, restart guard, double-start rejection.

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation
  • New environment
  • Refactoring

Alignment Checklist

Before submitting, verify:

  • I have read .claude/docs/PRINCIPLES.md and this PR aligns with our principles
  • I have checked .claude/docs/INVARIANTS.md and no invariants are violated
  • I have run /pre-submit-pr (or bash .claude/hooks/lint.sh and tests) and addressed all issues

RFC Status

  • Not required (bug fix)

Test Plan

  • PYTHONPATH=src:envs uv run pytest tests/test_core/test_hf_sandbox_provider.py -v: 10 passed, no HF account needed
  • PYTHONPATH=src:envs uv run pytest tests/test_core/ -q: 632 passed, 16 skipped (4 pre-existing local failures in test_generic_client.py from Docker occupying port 8000; they reproduce on main)
  • usort, ruff format --check, ruff check clean on both changed files
  • Live run against an HF sandbox (needs an account that can run Jobs): start an echo env through HFSandboxProvider, verify /health, echo_message round-trip, and clean stop_container()

Claude Code Review

N/A — changes were driven by maintainer review in #1556 and verified with real-transport probes (HTTPS router, plaintext and TLS recorders, websockets 15.0.1/16.0).

Note for #1556: the AgentSandboxProvider proxy copy there will match this file once this PR lands; that PR drops its hf_sandbox_provider.py edits accordingly.


Note

High Risk
Changes authentication proxy behavior (redirect rejection and error sanitization) in a path that injects HF credentials; mistakes could leak tokens or break sandbox connectivity.

Overview
Hardens _LocalAuthProxy in hf_sandbox_provider.py so injected Hugging Face auth headers cannot be replayed to another origin via redirects, and so failures do not leak credentials in logs or client errors.

Redirect handling: HTTP upstream calls use allow_redirects=False; 3xx responses become a generic 502 instead of following the redirect (which would resend auth headers and bodies, and could downgrade HTTPS). WebSocket handshakes disable websockets redirect following via process_redirect, for the same cross-origin header replay reason.

WebSocket errors: Upstream handshake failures are caught in the route, logged with a generic warning only, and the client is closed without propagating exception text that may embed secrets in URLs.

Lifecycle: stop() sets force_exit, raises RuntimeError if the proxy thread does not exit within 5s (keeping server/thread handles for retry instead of leaking a bound loopback port). HFSandboxProvider blocks start_container() while a proxy handle still awaits cleanup, and stop_container() still kills the sandbox when proxy shutdown fails, then re-raises the proxy error.

Adds tests/test_core/test_hf_sandbox_provider.py — loopback fakes (no HF account): header injection, HTTP/WS redirect rejection against a recorder “attacker”, sanitized handshake logging, stuck-thread behavior, and provider start/stop guards.

Reviewed by Cursor Bugbot for commit 94e607e. Bugbot is set up for automated code reviews on this repo. Configure here.

- Reject upstream redirects instead of following them: HTTP requests use
  allow_redirects=False (3xx becomes a generic 502) and WebSocket
  handshakes disable websockets' redirect following via process_redirect,
  which would otherwise replay the injected auth headers to another
  origin and could downgrade https to http.
- Sanitize WebSocket handshake failures: the route catches them, logs a
  generic warning without the exception chain (which can embed
  credentials in the target URL) and denies the client.
- stop() sets force_exit to bound the drain, and if the thread survives
  the join it keeps its handles and raises, so cleanup can retry instead
  of leaking a thread that keeps the loopback port bound.
  stop_container() still kills the sandbox first and re-raises after.
- start_container() refuses a restart while a proxy from a failed stop
  is still retained, so the old thread cannot be overwritten and lost.

Adds a dedicated regression test suite (faked pool, loopback fake
upstream, no HF account or network needed).

Signed-off-by: dongjiang1989 <dongjiang1989@126.com>
dongjiang1989 added a commit to dongjiang1989/OpenEnv that referenced this pull request Nov 5, 2026
The HF proxy hardening applies to code already on main and lands
separately in openenv#1558 with its own regression suite, keeping this PR
focused on the new provider.

Signed-off-by: dongjiang1989 <dongjiang1989@126.com>

@burtenshaw burtenshaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

src/openenv/core/containers/runtime/hf_sandbox_provider.py at 94e607e3 is identical to the version reviewed on #1556 at 0eda21ab.

That review verified it against a local HTTPS router and recorders with websockets 15.0.1 and 16.0. An HTTP 307 to a plaintext recorder and a WSS handshake 302 to a different TLS origin are both rejected, and the recorder receives no request, body or credential header. As a control, the default WebSocket connector follows the same 302 and replays Authorization. Failed handshakes return a generic denial, and captured log records and exception chains contain no upstream URL or token. Normal authenticated HTTP traffic, query strings, WebSocket text and downstream binary frames pass. stop() bounds its join and keeps the server and thread handles when the thread is still alive, and the _proxy is not None start guard refuses a restart until cleanup succeeds.

The new tests/test_core/test_hf_sandbox_provider.py adds the first regression suite for this provider. No blocking findings; this can merge once CI passes.

@burtenshaw
burtenshaw merged commit af3bac8 into openenv:main Nov 5, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants