From 636194a1472b28ecc5da25f269d76e01a77c9de9 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 15 Mar 2026 21:45:55 -0700 Subject: [PATCH] fix(server): authorize sandbox provider env requests by metadata --- crates/openshell-server/src/grpc.rs | 64 ++++++++++++++++++++++++++--- 1 file changed, 58 insertions(+), 6 deletions(-) diff --git a/crates/openshell-server/src/grpc.rs b/crates/openshell-server/src/grpc.rs index 422b6463..e3339368 100644 --- a/crates/openshell-server/src/grpc.rs +++ b/crates/openshell-server/src/grpc.rs @@ -56,6 +56,9 @@ use crate::ServerState; /// unbounded memory allocation from an excessively large page request. pub const MAX_PAGE_SIZE: u32 = 1000; +/// Metadata key used to bind internal requests to a specific sandbox identity. +const HEADER_SANDBOX_ID: &str = "x-sandbox-id"; + // --------------------------------------------------------------------------- // Field-level size limits // @@ -111,6 +114,22 @@ pub fn clamp_limit(raw: u32, default: u32, max: u32) -> u32 { if raw == 0 { default } else { raw.min(max) } } +fn authorize_sandbox_request(request: &Request, sandbox_id: &str) -> Result<(), Status> { + let caller_sandbox_id = request + .metadata() + .get(HEADER_SANDBOX_ID) + .and_then(|value| value.to_str().ok()) + .ok_or_else(|| Status::unauthenticated("missing required x-sandbox-id metadata"))?; + + if caller_sandbox_id != sandbox_id { + return Err(Status::permission_denied( + "sandbox identity does not match requested sandbox_id", + )); + } + + Ok(()) +} + /// OpenShell gRPC service implementation. #[derive(Debug, Clone)] pub struct OpenShellService { @@ -796,7 +815,8 @@ impl OpenShell for OpenShellService { &self, request: Request, ) -> Result, Status> { - let sandbox_id = request.into_inner().sandbox_id; + let sandbox_id = request.get_ref().sandbox_id.clone(); + authorize_sandbox_request(&request, &sandbox_id)?; let sandbox = self .state @@ -3311,15 +3331,15 @@ mod tests { MAX_ENVIRONMENT_ENTRIES, MAX_LOG_LEVEL_LEN, MAX_MAP_KEY_LEN, MAX_MAP_VALUE_LEN, MAX_NAME_LEN, MAX_PAGE_SIZE, MAX_POLICY_SIZE, MAX_PROVIDER_CONFIG_ENTRIES, MAX_PROVIDER_CREDENTIALS_ENTRIES, MAX_PROVIDER_TYPE_LEN, MAX_PROVIDERS, - MAX_TEMPLATE_MAP_ENTRIES, MAX_TEMPLATE_STRING_LEN, MAX_TEMPLATE_STRUCT_SIZE, clamp_limit, - create_provider_record, delete_provider_record, get_provider_record, is_valid_env_key, - list_provider_records, resolve_provider_environment, update_provider_record, - validate_provider_fields, validate_sandbox_spec, + MAX_TEMPLATE_MAP_ENTRIES, MAX_TEMPLATE_STRING_LEN, MAX_TEMPLATE_STRUCT_SIZE, + authorize_sandbox_request, clamp_limit, create_provider_record, delete_provider_record, + get_provider_record, is_valid_env_key, list_provider_records, resolve_provider_environment, + update_provider_record, validate_provider_fields, validate_sandbox_spec, }; use crate::persistence::Store; use openshell_core::proto::{Provider, SandboxSpec, SandboxTemplate}; use std::collections::HashMap; - use tonic::Code; + use tonic::{Code, Request}; #[test] fn env_key_validation_accepts_valid_keys() { @@ -3338,6 +3358,38 @@ mod tests { assert!(!is_valid_env_key("X;rm -rf /")); } + #[test] + fn authorize_sandbox_request_accepts_matching_metadata() { + let mut request = Request::new(()); + request + .metadata_mut() + .insert("x-sandbox-id", "sandbox-123".parse().unwrap()); + + assert!(authorize_sandbox_request(&request, "sandbox-123").is_ok()); + } + + #[test] + fn authorize_sandbox_request_rejects_missing_metadata() { + let request = Request::new(()); + let err = authorize_sandbox_request(&request, "sandbox-123").unwrap_err(); + + assert_eq!(err.code(), Code::Unauthenticated); + assert!(err.message().contains("x-sandbox-id")); + } + + #[test] + fn authorize_sandbox_request_rejects_mismatched_sandbox_id() { + let mut request = Request::new(()); + request + .metadata_mut() + .insert("x-sandbox-id", "sandbox-aaa".parse().unwrap()); + + let err = authorize_sandbox_request(&request, "sandbox-bbb").unwrap_err(); + + assert_eq!(err.code(), Code::PermissionDenied); + assert!(err.message().contains("does not match")); + } + // ---- clamp_limit tests ---- #[test]