Resolve every symlink in `sandbox_path` on the sandbox side and refuse the result if it lands outside `/sandbox`. The lexical guard in `validate_sandbox_source_path` cannot see symlinks; a path such as `/sandbox/etc-link/passwd` (where `etc-link -> /etc`) clears the lexical check but would still leak `/etc/passwd` once `tar -C` follows the link. Resolving symlinks on the remote side and re-valida
(
session: &SshSessionConfig,
sandbox_path: &str,
)
| 891 | /// that gap. The returned fully-resolved path is what the caller should hand |
| 892 | /// to probe and tar invocations. |
| 893 | async fn resolve_sandbox_source_path( |
| 894 | session: &SshSessionConfig, |
| 895 | sandbox_path: &str, |
| 896 | ) -> Result<String> { |
| 897 | let resolve_cmd = format!("realpath -e -- {path}", path = shell_escape(sandbox_path)); |
| 898 | let resolved = ssh_run_capture_stdout(session, &resolve_cmd) |
| 899 | .await |
| 900 | .wrap_err_with(|| format!("failed to resolve sandbox source path '{sandbox_path}'"))?; |
| 901 | if resolved.is_empty() { |
| 902 | return Err(miette::miette!( |
| 903 | "sandbox source path '{sandbox_path}' does not exist" |
| 904 | )); |
| 905 | } |
| 906 | if !is_under_sandbox_workspace(&resolved) { |
| 907 | return Err(miette::miette!( |
| 908 | "sandbox source path '{sandbox_path}' resolves to '{resolved}', outside the sandbox workspace ({SANDBOX_WORKSPACE_ROOT})" |
| 909 | )); |
| 910 | } |
| 911 | Ok(resolved) |
| 912 | } |
| 913 | |
| 914 | /// Resolve the host-side target path for a downloaded *file*, following |
| 915 | /// `cp`-style semantics. |
no test coverage detected