astrid-runtime/astrid · error

sandbox {label} contains forbidden characters (double-quote,

Error message

sandbox {label} contains forbidden characters (double-quote, backslash, or null): {}

What it means

Paths are interpolated verbatim into sandbox profile text (SBPL/bwrap), where double-quote, backslash, and NUL can break out of or corrupt the profile syntax and weaken the sandbox. validate_sandbox_str rejects any path containing these characters with InvalidInput.

Source

Thrown at crates/astrid-workspace/src/sandbox/mod.rs:32

/// all of which can break or bypass sandbox profile syntax.
fn validate_sandbox_str<'a>(path: &'a Path, label: &str) -> io::Result<&'a str> {
    if !path.is_absolute() {
        return Err(io::Error::new(
            io::ErrorKind::InvalidInput,
            format!(
                "sandbox {label} must be an absolute path, got: {}",
                path.display()
            ),
        ));
    }
    let s = path.to_str().ok_or_else(|| {
        io::Error::new(
            io::ErrorKind::InvalidInput,
            format!("sandbox {label} is not valid UTF-8: {}", path.display()),
        )
    })?;
    if s.contains(['"', '\\', '\0']) {
        return Err(io::Error::new(
            io::ErrorKind::InvalidInput,
            format!(
                "sandbox {label} contains forbidden characters (double-quote, backslash, or null): {}",
                path.display()
            ),
        ));
    }
    Ok(s)
}

/// A host-verified, read-only file the sandbox materializes inside a spawned
/// child. `source` is the host-owned path the verified snapshot already lives
/// at (the in-sandbox bytes are bound/copied FROM here); `target` is the
/// absolute path inside the child's sandbox at which it reads those bytes.
///
/// The caller is responsible for ensuring `source` is a host-owned location
/// the child and the spawning principal's capsule fs surface cannot write —
/// the sandbox layer only wires the read-only exposure, it does not snapshot.

View on GitHub (pinned to affd8760f4)

Solutions

  1. Remove or replace the forbidden characters in the path (e.g. normalize backslashes to forward slashes)
  2. Reject the path at your own input boundary: if p.as_os_str().to_string_lossy().contains(['"','\\','\0']) bail early
  3. Never build paths by string-concatenating untrusted input; use PathBuf::join
  4. If escaping is genuinely needed, do not escape yourself — the library intentionally refuses, so use a path without these characters

Example fix

// before
let p = PathBuf::from(user_supplied); // may contain " or \
wrap_with_process_paths(&ws, &[p], &[])?;
// after
let cleaned = user_supplied.replace('\\', "/");
if cleaned.contains(['"', '\\', '\0']) { return Err(...); }
wrap_with_process_paths(&ws, &[PathBuf::from(cleaned)], &[])?;
Defensive patterns

Strategy: validation

Validate before calling

fn sandbox_safe(p: &Path) -> bool { p.to_str().map_or(false, |s| !s.contains(['"', '\\', '\0'])) }

Type guard

fn has_forbidden_chars(p: &Path) -> bool { p.to_str().map_or(false, |s| s.contains(['"', '\\', '\0'])) }

Try / catch

if let Err(e) = wrap_with_process_paths(&ws, &paths, &[]) { if e.to_string().contains("forbidden characters") { /* reject the path; do not attempt to escape it yourself */ } return Err(e); }

Prevention

When it happens

Trigger: Calling wrap_with_process_paths, validate_all_paths, or build_seatbelt_prefix with a path containing '"', '\\', or '\0' — e.g. a Windows-style path with backslashes used on the macOS Seatbelt path, or a crafted path from untrusted input.

Common situations: Untrusted user input used as a path; copy-pasted Windows paths with backslashes; generated paths from templates that accidentally include quotes; paths assembled from binary data retaining NULs.

Understand the failure class

Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.

Related errors


AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09). Data as JSON: /api/errors/bf955586099db1b0. Report an issue: GitHub.