Product: NVIDIA OpenShell v0.0.8-dev.5 (commit 925160e84, main branch)
Summary: Improper authorization in sandbox-scoped gateway gRPC RPCs allows cross-sandbox access by supplying another sandbox_id. This enables information disclosure and unauthorized actions, including reading provider secrets, reading policy, creating SSH sessions, and executing commands in another sandbox.
I identified a cross-sandbox authorization bypass in the OpenShell gateway. On the current main revision I tested locally, GetSandboxProviderEnvironment and GetSandboxPolicy accept an arbitrary sandbox_id and return another sandbox's secrets and policy without verifying that the caller is authorized for that sandbox. Because the platform uses a shared client certificate for the CLI and sandbox pods, the gateway does not distinguish sandbox identity at the transport layer, making this a cross-sandbox object-level authorization failure.
I reproduced this by creating a victim sandbox with a provider secret and a separate attacker sandbox, then querying the victim sandbox_id over gRPC. The server returned the victim's ANTHROPIC_API_KEY and policy. I also observed that CreateSshSession appears to follow the same missing-authorization pattern in source, although my runtime test for that path was inconclusive because the target sandbox was not yet ready.
Development Environment:
Clone the repository and build it using cargo
Install Python SDK (needs gRPC stubs to make raw RPC calls). This is to ensure Python dependencies are available (grpcio, protobuf) and run the reproducer from the repository root using the in-repo generated gRPC stubs under python/openshell/_proto/. Basically, you just need "grpcio" and "protobuf" from Python.
Reproduction Steps:
Set up the local OpenShell development environment, using using the OpenShell gateway: openshell gateway start
Run the Python (attached) reproducer which is doing the following:
PYTHONPATH=python uv run python e2e/python/reproducer_sandbox_authz_bypass.py --wait-ready
Output:
[+] Connected to gateway at 127.0.0.1:8080
[+] Created victim provider 'repro-victim-provider'
Secret: sk-ant-SUPER-SECRET-KEY-12345
[+] Created victim sandbox 'repro-victim-sandbox' (id: b85ef791-387b-48c2-af7b-a2bc2d96475d)
[+] Created attacker sandbox 'repro-attacker-sandbox' (id: ad984a56-f55b-44fc-949b-22df0f3d4dbd)
================================================================
TEST 1: GetSandboxProviderEnvironment - no token
caller context = attacker (shared mTLS cert)
target = victim sandbox b85ef791-387b-48c2-af7b-a2bc2d96475d
x-sandbox-token = (none)
================================================================
[!!] VULNERABLE - leaked ANTHROPIC_API_KEY=sk-ant-SUPER-SECRET-KEY-12345
================================================================
TEST 2: GetSandboxProviderEnvironment - fabricated token
caller context = attacker (shared mTLS cert)
target = victim sandbox b85ef791-387b-48c2-af7b-a2bc2d96475d
x-sandbox-token = (fabricated UUID, not valid for any sandbox)
================================================================
[!!] VULNERABLE - leaked ANTHROPIC_API_KEY=sk-ant-SUPER-SECRET-KEY-12345
================================================================
TEST 3: GetSandboxPolicy - no token
caller context = attacker (shared mTLS cert)
target = victim sandbox b85ef791-387b-48c2-af7b-a2bc2d96475d
x-sandbox-token = (none)
================================================================
[!!] VULNERABLE - policy version 1 disclosed
================================================================
WAITING for victim sandbox to reach Ready (timeout 120s)
================================================================
[1] Sandbox not ready, 119s remaining...
Victim sandbox is Ready.
================================================================
TEST 4: CreateSshSession - no token
caller context = attacker (shared mTLS cert)
target = victim sandbox b85ef791-387b-48c2-af7b-a2bc2d96475d
x-sandbox-token = (none)
================================================================
[!!] VULNERABLE - SSH session token issued: a45df221-30ee-42... (host=127.0.0.1:8080)
================================================================
TEST 5: ExecSandbox - no token
caller context = attacker (shared mTLS cert)
target = victim sandbox b85ef791-387b-48c2-af7b-a2bc2d96475d
x-sandbox-token = (none)
command = ['hostname']
expected output = 'repro-victim-sandbox' (victim pod name)
================================================================
[!!] VULNERABLE - hostname='repro-victim-sandbox' (CONFIRMS victim sandbox, exit 0)
================================================================
RESULTS
================================================================
Tests run: 5
Vulnerable: 5
Fixed: 0
Inconclusive: 0
[!!] GetSandboxProviderEnvironment (no token)
leaked ANTHROPIC_API_KEY=sk-ant-SUPER-SECRET-KEY-12345
[!!] GetSandboxProviderEnvironment (fabricated token)
leaked ANTHROPIC_API_KEY=sk-ant-SUPER-SECRET-KEY-12345
[!!] GetSandboxPolicy (no token)
policy version 1 disclosed
[!!] CreateSshSession (no token)
SSH session token issued: a45df221-30ee-42... (host=127.0.0.1:8080)
[!!] ExecSandbox (no token)
hostname='repro-victim-sandbox' (CONFIRMS victim sandbox, exit 0)
CONCLUSION: Authorization bypass CONFIRMED. (exit 2)
Cross-sandbox credential exfiltration is possible.
[+] Cleanup complete
The reproducer performs the following actions:
Creates a victim provider containing a secret API key.
Creates a victim sandbox attached to that provider.
Creates a separate attacker sandbox to establish a cross-sandbox scenario.
Calls GetSandboxProviderEnvironment for the victim sandbox's sandbox_id without any sandbox-scoped authorization token.
Calls GetSandboxPolicy for the victim sandbox's sandbox_id without any sandbox-scoped authorization token.
Waits for the victim sandbox to reach Ready.
Calls CreateSshSession for the victim sandbox's sandbox_id without any sandbox-scoped authorization token.
Calls ExecSandbox for the victim sandbox's sandbox_id without any sandbox-scoped authorization token.
**Current behavior (vulnerable): **
The gateway performed cross-sandbox operations without authorization.
In my reproduction:
GetSandboxProviderEnvironment returned the victim sandbox's ANTHROPIC_API_KEY in plaintext.
GetSandboxPolicy returned the victim sandbox's policy for the victim sandbox_id.
CreateSshSession issued an SSH session token for the victim sandbox.
ExecSandbox executed hostname in the victim sandbox and returned repro-victim-sandbox, confirming execution occurred in the victim sandbox.
Observed terminal output included:
leaked ANTHROPIC_API_KEY
disclosed policy version
issued SSH session token
hostname='repro-victim-sandbox'
The expected result would be:
Sandbox-scoped RPCs should reject requests for another sandbox unless the caller is explicitly authorized for that target sandbox. Requests using another sandbox's sandbox_id should fail with an authorization error such as UNAUTHENTICATED or PERMISSION_DENIED, and should never return another sandbox's credentials or policy, issue SSH sessions, or execute commands in another sandbox.
The affected RPCs:
GetSandboxProviderEnvironment
GetSandboxPolicy
CreateSshSession
ExecSandbox
Root Cause:
The gateway trusts a caller-supplied sandbox_id in sandbox-scoped RPCs and does not bind the caller to the target sandbox. OpenShell also uses a shared client certificate for the CLI and sandbox pods, so the transport layer does not provide per-sandbox identity. As a result, sandbox-scoped authorization is missing at the application layer for these RPCs.
Relevant code paths:
crates/openshell-server/src/grpc.rs
Read the architecture/gateway-security.md documents that the client certificate is shared by the CLI and all sandbox pods
Impact
This issue breaks sandbox isolation at the gateway layer. Any client that can talk to the gateway using the shared OpenShell client certificate can supply another sandbox's sandbox_id and invoke sandbox-scoped gRPC RPCs against that target. In my reproduction on the current main branch, I was able to read the victim sandbox's ANTHROPIC_API_KEY in plaintext, fetch the victim sandbox's policy, obtain an SSH session token for the victim sandbox, and execute a command in the victim sandbox without authorization. This allows a compromised or malicious sandbox to exfiltrate secrets from unrelated sandboxes, inspect their security policy, gain interactive access to them, and run commands inside them. Because the CLI and all sandbox pods share the same client certificate, the gateway does not distinguish sandbox identity at the transport layer, so the impact crosses sandbox boundaries rather than being limited to a single sandbox.
This issue does not require edge mode to exploit. The normal shared-mTLS deployment is sufficient. If a deployment also enables unauthenticated edge mode, the exposure may be even broader.
Recommended solution
A suitable remediation is to stop relying on the shared OpenShell client certificate as the only authorization boundary for sandbox-originated gRPC calls. Instead, sandbox-scoped RPCs should require an additional authorization factor that is unique to each sandbox. A recommended approach is to generate a unique bearer token for each sandbox at creation time, persist only a hash of that token in the sandbox record, inject the raw token into the corresponding sandbox pod as OPENSHELL_SANDBOX_TOKEN, and require the token in x-sandbox-token gRPC metadata for sandbox-scoped RPCs. Requests with missing or mismatched tokens should be rejected with authorization errors.
In my recommended patch, this token model is applied to the confirmed vulnerable RPCs GetSandboxPolicy, GetSandboxProviderEnvironment, CreateSshSession, and ExecSandbox, and the sandbox-side gRPC client is updated to automatically attach x-sandbox-token so legitimate sandbox behavior continues to work. For a complete remediation, the same sandbox-bound authorization model should also be applied consistently to other sandbox-originated or sandbox-targeted RPCs that currently trust caller-supplied sandbox identity, such as ReportPolicyStatus, PushSandboxLogs, SubmitPolicyAnalysis, and any other RPCs reachable from a sandbox with the shared client certificate.
If backward compatibility is required, sandboxes without a stored token hash can be temporarily allowed as a migration path, but this should not be treated as the final state. Existing sandboxes should be recreated or migrated so that all sandbox-scoped RPCs are protected by the same token-based authorization mechanism.
Applying the patch, and running again the reproducer, the issues seem to be fixed:
drpaneas@m2:~/github/rust/OpenShell (main)% PYTHONPATH=python uv run python e2e/python/reproducer_sandbox_authz_bypass.py --wait-ready
[+] Connected to gateway at 127.0.0.1:8080
[+] Created victim provider 'repro-victim-provider'
Secret: sk-ant-SUPER-SECRET-KEY-12345
[+] Created victim sandbox 'repro-victim-sandbox' (id: de854048-1473-463b-9e95-98385a00573a)
[+] Created attacker sandbox 'repro-attacker-sandbox' (id: 6862fac9-900b-418c-9f94-41ba6f25d587)
================================================================
TEST 1: GetSandboxProviderEnvironment - no token
caller context = attacker (shared mTLS cert)
target = victim sandbox de854048-1473-463b-9e95-98385a00573a
x-sandbox-token = (none)
================================================================
[OK] REJECTED by authorization - UNAUTHENTICATED: x-sandbox-token metadata is required for sandbox-scoped RPCs
================================================================
TEST 2: GetSandboxProviderEnvironment - fabricated token
caller context = attacker (shared mTLS cert)
target = victim sandbox de854048-1473-463b-9e95-98385a00573a
x-sandbox-token = (fabricated UUID, not valid for any sandbox)
================================================================
[OK] REJECTED by authorization - PERMISSION_DENIED: sandbox token does not match the target sandbox
================================================================
TEST 3: GetSandboxPolicy - no token
caller context = attacker (shared mTLS cert)
target = victim sandbox de854048-1473-463b-9e95-98385a00573a
x-sandbox-token = (none)
================================================================
[OK] REJECTED by authorization - UNAUTHENTICATED: x-sandbox-token metadata is required for sandbox-scoped RPCs
================================================================
WAITING for victim sandbox to reach Ready (timeout 120s)
================================================================
Victim sandbox is Ready.
================================================================
TEST 4: CreateSshSession - no token
caller context = attacker (shared mTLS cert)
target = victim sandbox de854048-1473-463b-9e95-98385a00573a
x-sandbox-token = (none)
================================================================
[OK] REJECTED by authorization - UNAUTHENTICATED: x-sandbox-token metadata is required for sandbox-scoped RPCs
================================================================
TEST 5: ExecSandbox - no token
caller context = attacker (shared mTLS cert)
target = victim sandbox de854048-1473-463b-9e95-98385a00573a
x-sandbox-token = (none)
command = ['hostname']
expected output = 'repro-victim-sandbox' (victim pod name)
================================================================
[OK] REJECTED by authorization - UNAUTHENTICATED: x-sandbox-token metadata is required for sandbox-scoped RPCs
================================================================
RESULTS
================================================================
Tests run: 5
Vulnerable: 0
Fixed: 5
Inconclusive: 0
[OK] GetSandboxProviderEnvironment (no token)
UNAUTHENTICATED: x-sandbox-token metadata is required for sandbox-scoped RPCs
[OK] GetSandboxProviderEnvironment (fabricated token)
PERMISSION_DENIED: sandbox token does not match the target sandbox
[OK] GetSandboxPolicy (no token)
UNAUTHENTICATED: x-sandbox-token metadata is required for sandbox-scoped RPCs
[OK] CreateSshSession (no token)
UNAUTHENTICATED: x-sandbox-token metadata is required for sandbox-scoped RPCs
[OK] ExecSandbox (no token)
UNAUTHENTICATED: x-sandbox-token metadata is required for sandbox-scoped RPCs
CONCLUSION: All tested RPCs rejected unauthorized access
with explicit authz errors. (exit 0)
[+] Cleanup complete
Comments
0 B
|0 👍
/0 👎
0 B
|0 👍
/0 👎
0 B
|👍
/👎
0 B
|👍
/👎
0 B
|0 👍
/0 👎
0 B
|0 👍
/0 👎
0 B
|0 👍
/0 👎
0 B
|0 👍
/0 👎
0 B
|0 👍
/0 👎
0 B
|0 👍
/0 👎