Sandbox Escape in Actix
How Sandbox Escape Manifests in Actix
Actix-web's powerful async runtime and macro-based request handling create unique sandbox escape vectors. The framework's use of web::block for blocking operations and actix_rt::spawn for async tasks can inadvertently break the intended sandbox when misconfigured.
The most common sandbox escape in Actix occurs through improper use of web::block. When blocking operations are executed without proper isolation, they can access system resources outside the intended sandbox. Consider this vulnerable pattern:
async fn process_request(req: web::Json<Request>) -> impl Responder {
let result = web::block(move || {
// This runs in the blocking thread pool
let data = unsafe { std::fs::read("/etc/passwd") };
Ok(data)
}).await?;
HttpResponse::Ok().body(result)
}
The blocking thread pool in Actix shares resources across all handlers, creating a potential sandbox escape when one handler accesses files or system resources it shouldn't. An attacker who can influence the request path or payload might trigger file reads outside the intended directory.
Another Actix-specific vector involves the actix_rt::spawn function. When spawning background tasks without proper scope limitation, tasks can outlive the request context and access resources they shouldn't:
async fn start_background_task() -> impl Responder {
let task_handle = actix_rt::spawn(async {
// Task runs indefinitely, potentially accessing sensitive data
loop {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
});
// Task handle is dropped, but the task continues running
HttpResponse::Ok().finish()
}
The Actix extractor system can also create sandbox escapes when custom extractors aren't properly sandboxed. A malicious extractor could access request data beyond its intended scope:
impl<'a> FromRequest<'a> for SensitiveData {
type Error = actix_web::Error;
type Future = Ready<Result<Self, Self::Error>>;
type Config = ();
fn from_request(req: &'a HttpRequest, payload: &'a mut Payload) -> Self::Future {
// This extractor can access any part of the request
// including headers, query params, and body
let headers = req.headers().clone();
ready(Ok(SensitiveData { headers }))
}
}
The macro system in Actix can obscure these escape paths. The #[get], #[post] macros generate code that might not clearly show where blocking operations occur, making it harder to identify potential sandbox escapes during code review.
Actix-Specific Detection
Detecting sandbox escapes in Actix requires understanding both the framework's async patterns and its blocking operation handling. The most effective approach combines static analysis with runtime scanning.
Static analysis should focus on identifying web::block usage patterns and actix_rt::spawn calls. Tools like clippy can be configured with custom lints to flag potentially dangerous patterns:
use clippy_utils::diagnostics::span_lint_and_then;
use clippy_utils::source::snippet;
use rustc_hir::intravisit::FnKind;
use rustc_lint::LateContext;
#[derive(Default)]
pub struct SandboxEscapeDetector;
impl LateLintPass for SandboxEscapeDetector {
fn check_fn(&mut self, cx: &LateContext, kind: FnKind, _: &hir::FnDecl, body_id: hir::BodyId, span: Span, _: ast::Name) {
let body = cx.tcx.hir().body(body_id);
// Check for web::block usage
for expr in body.expr.walk() {
if let ExprKind::Call(path, args) = expr.kind {
if let ExprKind::Path(qself, path) = path.kind {
if path.segments.last().unwrap().ident.as_str() == "block" {
span_lint_and_then(
cx,
"sandbox_escape",
expr.span,
"Potential sandbox escape via web::block",
|diag| {
diag.help("Ensure blocking operations are properly sandboxed");
},
);
}
}
}
}
}
}
Runtime scanning with middleBrick can identify sandbox escape vulnerabilities by testing Actix endpoints for unauthorized resource access. The scanner tests for:
- Path traversal attempts through request parameters
- Unauthorized file system access patterns
- Blocking operation timeouts that might indicate sandbox breaks
- Background task persistence beyond request lifecycle
middleBrick's Actix-specific checks include scanning for common blocking operation patterns and testing extractor implementations for scope violations. The scanner can detect when an Actix endpoint might be exposing system resources it shouldn't.
Integration testing is crucial for Actix sandbox detection. Create test cases that attempt to access resources outside the intended sandbox:
#[actix_rt::test]
async fn test_sandbox_escape_prevention() {
let app = test::init_service(
App::new()
.service(web::resource("/test").route(web::post().to(process_request)))
).await;
// Test for path traversal
let req = test::TestRequest::post()
.uri("/test")
.set_json(&Request { path: String::from("../../etc/passwd") })
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
Actix-Specific Remediation
Remediating sandbox escapes in Actix requires a multi-layered approach that combines proper async patterns, resource isolation, and secure configuration.
The first layer of defense is proper blocking operation handling. Instead of using web::block directly, create isolated blocking pools with restricted permissions:
use actix_web::web;
use tokio::task::Builder;
use std::fs;
async fn safe_process_request(req: web::Json<Request>) -> impl Responder {
// Use a dedicated thread pool with restricted permissions
let result = Builder::new_multi_thread()
.enable_all()
.build()
.unwrap()
.spawn_blocking(move || {
// Only allow access to specific directories
let allowed_path = "/app/data";
let target_path = format!("{}/{}", allowed_path, req.filename);
if target_path.starts_with(allowed_path) {
fs::read(target_path)
} else {
Err(std::io::Error::new(std::io::ErrorKind::PermissionDenied, "Access denied"))
}
})
.await?;
HttpResponse::Ok().body(result)
}
For background tasks, use scoped execution that ties task lifetime to the request context:
use actix_web::web;
use actix_rt::spawn;
use tokio::time::sleep;
async fn start_scoped_background_task() -> impl Responder {
// Use a cancellation token to ensure task termination
let (tx, rx) = tokio::sync::oneshot::channel::<()>();
spawn(async move {
tokio::select! {
_ = rx => {
// Task cancelled gracefully
println!("Background task terminated");
}
_ = sleep(std::time::Duration::from_secs(5)) => {
// Task completes naturally
println!("Background task completed");
}
}
});
// Return cancellation token to caller if needed
HttpResponse::Ok().body("Task started")
}
Custom extractors should implement strict scope limitations:
use actix_web::{FromRequest, HttpRequest, dev::Payload, error, http::header};
use futures_util::future::{ready, Ready};
pub struct ScopedData {
pub headers: Vec,
}
impl<'a> FromRequest<'a> for ScopedData {
type Error = actix_web::Error;
type Future = Ready<Result<Self, Self::Error>>;
type Config = ();
fn from_request(req: &'a HttpRequest, _: &'a mut Payload) -> Self::Future {
// Only allow access to specific headers
let allowed_headers = [
header::CONTENT_TYPE,
header::USER_AGENT,
header::ACCEPT,
];
let headers: Vec<_> = allowed_headers
.iter()
.filter_map(|h| req.headers().get(*h))
.collect();
ready(Ok(ScopedData { headers }))
}
}
For comprehensive sandbox protection, implement a middleware that validates all requests against a security policy:
use actix_web::{dev::ServiceRequest, dev::ServiceResponse, middleware::Middleware};
use actix_web::Error;
use futures_util::future::BoxFuture;
pub struct SandboxProtection;
impl Middleware<S> for SandboxProtection
where
S: actix_web::dev::Service<Request = ServiceRequest, Response = ServiceResponse<B>>,
S::Future: 'static,
S::Error: Into<actix_web::Error>,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;
fn call(&self, req: ServiceRequest, srv: &mut S) -> Self::Future {
// Check for sandbox escape patterns
let path = req.uri().path();
if path.contains("..") || path.contains("/../") {
let res = req.error_response(error::ErrorBadRequest("Sandbox violation"));
return Box::pin(async move { Ok(res) });
}
// Validate request size and content
if let Some(len) = req.headers().get(header::CONTENT_LENGTH) {
if let Ok(len) = len.to_str().parse::() {
if len > 1_000_000 { // 1MB limit
let res = req.error_response(error::ErrorPayloadTooLarge("Request too large"));
return Box::pin(async move { Ok(res) });
}
}
}
// Continue processing if checks pass
Box::pin(srv.call(req))
}
}
Frequently Asked Questions
How does sandbox escape differ between Actix and other Rust web frameworks?
web::block function for blocking operations and actix_rt::spawn for background tasks can inadvertently break sandbox boundaries when misused. Unlike frameworks like Rocket or Warp, Actix's shared thread pool for blocking operations means a single misconfigured handler can potentially access system resources across the entire application. The macro system in Actix can also obscure where blocking operations occur, making sandbox escapes harder to detect during code review.