fix(sandbox): require bearer-token auth on sandbox API (fixes #78)#85
fix(sandbox): require bearer-token auth on sandbox API (fixes #78)#85pratyush618 wants to merge 6 commits intohuggingface:mainfrom
Conversation
The sandbox's embedded server exposed /api/bash, /api/read, /api/write, /api/edit, /api/kill, /api/exists, /api/health without validating the Authorization header the client already sends. Anyone who could reach the public *.hf.space URL could execute arbitrary commands inside the user's sandbox — and read HF_TOKEN from the environment. Add an app-wide FastAPI dependency that reads HF_TOKEN at startup and matches it against the Bearer token on every request using hmac.compare_digest. Fail closed: if HF_TOKEN is unset, every request 401s rather than silently becoming open. Refs huggingface#78
Six cases against the _SANDBOX_SERVER source loaded via importlib: - missing Authorization header → 401 - wrong Bearer token → 401 - correct Bearer token → 200 - /api/bash with wrong token must 401 before subprocess.Popen is called - fail-closed when HF_TOKEN is unset → every request 401s - /api/write spot-check to confirm the app-wide dependency covers POST routes and the target file is never written
Defense in depth alongside the server-side bearer-token check: make Sandbox.create(..., private=True) the default so the HF Space platform itself also gates access to the *.hf.space URL. The client already sends the user's HF_TOKEN in the Bearer header, which is what HF's gateway validates for private-space access, so existing flows stay working. sandbox_create_handler only forwards 'private' when the caller explicitly passes it, so this default propagates cleanly without any handler change.
|
Thanks for the review @statikkkkk. On the two concrete points:
|
|
@claude please review this PR. Focus on correctness, security, LiteLLM/Bedrock routing impact, and whether it's safe to merge against current main. Keep it concise and prefer inline comments where it matters. |
|
Claude finished @akseljoonas's task in 2m 0s —— View job PR Review: fix(sandbox): require bearer-token auth on sandbox API
Verdict: APPROVE with one important fix noted. The core security fix is correct and well-implemented. One issue could cause a silent hang on mis-auth; the rest are minor. Security — ✅ Correct
Bug:
|
| Area | Status |
|---|---|
Auth logic (hmac.compare_digest, fail-closed) |
✅ Correct |
| Scope (app-wide FastAPI dependency) | ✅ Correct |
| Token injection timing | ✅ Correct |
private=True defense-in-depth |
✅ Correct |
_wait_for_api on 401 |
|
| Tool spec description | 📝 Stale description, minor |
| LiteLLM/Bedrock impact | ✅ None |
| Tests | ✅ Good, one gap |
Safe to merge after addressing the _wait_for_api 401 hang (or accepting it as a known rough edge).
Before this change, a reachable sandbox server that returned 401/403 on /api/health (e.g. because HF_TOKEN was not set as a Space secret, or the client and server tokens don't match) would cause the health-poll loop to spin for the full API_WAIT_TIMEOUT and then raise "not responding" — a misleading error when the real cause is auth. Short-circuit on 401/403 with a RuntimeError that points at the likely misconfiguration. TimeoutError still fires for genuine unreachability.
The 'private' parameter description ("If true, create a private Space")
implied false was the default. Since Sandbox.create now defaults to
private=True, the schema an LLM sees should match. Update the description
to state the true default and how to opt out.
The auth tests so far only cover the rejection paths. Add a matching positive case that confirms /api/bash actually runs a command and returns its stdout when the Bearer token is correct, so a regression that broke the happy path (e.g. require_auth accidentally rejecting valid tokens) would show up as a test failure instead of only manifesting at runtime.
|
Addressed the review in three follow-up commits on the same branch:
|
Summary
Fixes #78 — the sandbox's embedded FastAPI server exposed command-execution and file I/O endpoints (
/api/bash,/api/read,/api/write,/api/edit,/api/kill,/api/exists,/api/health) without validating theAuthorizationheader the client already sends. Combined withSandbox.create(..., private=False)as the default, any unauthenticated caller who could reachhttps://<slug>.hf.space/could execute arbitrary commands in a user's sandbox andprintenv HF_TOKENto pivot to the user's HF account.Three small, reviewable commits:
fix(sandbox): require bearer-token auth on embedded FastAPI routes— app-wide FastAPI dependency readsHF_TOKENat startup and validates theBearertoken withhmac.compare_digest. Fails closed whenHF_TOKENis unset.test(sandbox): cover bearer-token auth on embedded server— 6 pytest cases against the_SANDBOX_SERVERsource loaded viaimportlib. Covers missing / wrong / correct bearer,/api/bashnever reachingsubprocess.Popenwithout auth,/api/writespot-check, and the fail-closed path.fix(sandbox): default new sandboxes to private— defense in depth: makeSandbox.create(..., private=True)the default so the HF Space gateway also gates access. Client already sends the user'sHF_TOKENas Bearer, so existing flows keep working.No client-side header change was needed:
Sandbox.__post_init__already sentAuthorization: Bearer {self.token}whereself.tokenis the user'sHF_TOKEN, which is exactly what the new server dependency validates against theHF_TOKENsecret already injected bysandbox_tool.py.Behavior change
Sandbox.create(...)now defaults toprivate=True. Callers that relied on the old public-by-default behavior must now passprivate=Falseexplicitly. The agent-facingsandbox_createtool is unaffected in shape: itsprivateparameter already defaults toTruevia this change, and the tool only forwardsprivatewhen the caller passes it. There is noCHANGELOGfile in the repo; flagging here for release notes.Test plan
uv run --with pytest --with pytest-asyncio pytest tests/ -q→ 17 passed (6 new + 11 existing).compile(_SANDBOX_SERVER, "sandbox_server.py", "exec")— the embedded server source is valid Python.sandbox_createvia the agent, confirm the Space is private, thencurl -X POST https://<slug>.hf.space/api/bash -d '{"command":"id"}'without a Bearer token → expect401(or HF gateway 401/redirect). With the correct Bearer → expect200.bash,read,write,editinside the sandbox after the change.