Skip to main content

gravityfile_plugin/
sandbox.rs

1//! Security sandboxing for plugins.
2//!
3//! This module provides sandboxing capabilities to restrict what plugins
4//! can access and do.
5
6use std::collections::HashSet;
7use std::path::PathBuf;
8
9use serde::{Deserialize, Serialize};
10
11/// Permission types that can be granted to plugins.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
13#[serde(rename_all = "lowercase")]
14pub enum Permission {
15    /// Read files from the filesystem.
16    Read,
17
18    /// Write files to the filesystem.
19    Write,
20
21    /// Execute external commands.
22    Execute,
23
24    /// Access network resources.
25    Network,
26
27    /// Access environment variables.
28    Environment,
29
30    /// Access clipboard.
31    Clipboard,
32
33    /// Modify UI elements.
34    Ui,
35
36    /// Send notifications.
37    Notify,
38}
39
40/// Configuration for plugin sandboxing.
41#[derive(Debug, Clone)]
42pub struct SandboxConfig {
43    /// Paths the plugin is allowed to read from.
44    pub allowed_read_paths: Vec<PathBuf>,
45
46    /// Paths the plugin is allowed to write to.
47    pub allowed_write_paths: Vec<PathBuf>,
48
49    /// Commands the plugin is allowed to execute.
50    pub allowed_commands: HashSet<String>,
51
52    /// Whether network access is allowed.
53    pub allow_network: bool,
54
55    /// Whether environment access is allowed.
56    pub allow_env: bool,
57
58    /// Maximum execution time in milliseconds.
59    pub timeout_ms: u64,
60
61    /// Maximum memory in bytes (0 = unlimited).
62    pub max_memory: usize,
63
64    /// Maximum file size that can be read.
65    pub max_read_size: usize,
66
67    /// Granted permissions.
68    pub permissions: HashSet<Permission>,
69}
70
71impl Default for SandboxConfig {
72    fn default() -> Self {
73        let mut permissions = HashSet::new();
74        permissions.insert(Permission::Read);
75        permissions.insert(Permission::Notify);
76
77        Self {
78            allowed_read_paths: vec![],
79            allowed_write_paths: vec![],
80            allowed_commands: HashSet::new(),
81            allow_network: false,
82            allow_env: false,
83            timeout_ms: 5000,
84            max_memory: 256 * 1024 * 1024,   // 256 MB
85            max_read_size: 10 * 1024 * 1024, // 10 MB
86            permissions,
87        }
88    }
89}
90
91impl SandboxConfig {
92    /// Create a minimal sandbox with no permissions.
93    pub fn minimal() -> Self {
94        Self {
95            allowed_read_paths: vec![],
96            allowed_write_paths: vec![],
97            allowed_commands: HashSet::new(),
98            allow_network: false,
99            allow_env: false,
100            timeout_ms: 1000,
101            max_memory: 64 * 1024 * 1024,
102            max_read_size: 1024 * 1024,
103            permissions: HashSet::new(),
104        }
105    }
106
107    /// Create a permissive sandbox for trusted plugins.
108    ///
109    /// WARNING: This preset grants broad filesystem read access and environment
110    /// variable access. It does NOT grant execute or wildcard command access.
111    /// Use only with fully audited, first-party plugins. For untrusted plugins
112    /// use [`SandboxConfig::default`] or [`SandboxConfig::minimal`] instead.
113    pub fn permissive() -> Self {
114        let mut permissions = HashSet::new();
115        permissions.insert(Permission::Read);
116        permissions.insert(Permission::Write);
117        permissions.insert(Permission::Environment);
118        permissions.insert(Permission::Ui);
119        permissions.insert(Permission::Notify);
120
121        Self {
122            allowed_read_paths: vec![PathBuf::from("/")],
123            allowed_write_paths: vec![],
124            allowed_commands: HashSet::new(),
125            allow_network: false,
126            allow_env: true,
127            timeout_ms: 30000,
128            max_memory: 1024 * 1024 * 1024,
129            max_read_size: 100 * 1024 * 1024,
130            permissions,
131        }
132    }
133
134    /// Add an allowed read path.
135    pub fn allow_read(mut self, path: impl Into<PathBuf>) -> Self {
136        self.allowed_read_paths.push(path.into());
137        self.permissions.insert(Permission::Read);
138        self
139    }
140
141    /// Add an allowed write path.
142    pub fn allow_write(mut self, path: impl Into<PathBuf>) -> Self {
143        self.allowed_write_paths.push(path.into());
144        self.permissions.insert(Permission::Write);
145        self
146    }
147
148    /// Allow a specific command.
149    pub fn allow_command(mut self, cmd: impl Into<String>) -> Self {
150        self.allowed_commands.insert(cmd.into());
151        self.permissions.insert(Permission::Execute);
152        self
153    }
154
155    /// Enable network access.
156    pub fn allow_network(mut self) -> Self {
157        self.allow_network = true;
158        self.permissions.insert(Permission::Network);
159        self
160    }
161
162    /// Set timeout.
163    pub fn with_timeout(mut self, timeout_ms: u64) -> Self {
164        self.timeout_ms = timeout_ms;
165        self
166    }
167
168    /// Set memory limit.
169    pub fn with_memory_limit(mut self, bytes: usize) -> Self {
170        self.max_memory = bytes;
171        self
172    }
173
174    /// Grant a permission.
175    pub fn grant(mut self, permission: Permission) -> Self {
176        self.permissions.insert(permission);
177        self
178    }
179
180    /// Check if a permission is granted.
181    pub fn has_permission(&self, permission: Permission) -> bool {
182        self.permissions.contains(&permission)
183    }
184
185    /// Check if reading a path is allowed.
186    ///
187    /// Paths are canonicalized before comparison to prevent traversal attacks
188    /// via symlinks or `..` components.
189    ///
190    /// Callers must explicitly list allowed paths: an empty `allowed_read_paths`
191    /// list means no paths are allowed (deny-by-default), not unrestricted access.
192    pub fn can_read(&self, path: &std::path::Path) -> bool {
193        if !self.has_permission(Permission::Read) {
194            return false;
195        }
196        // Empty path list means no paths are explicitly permitted — deny all.
197        if self.allowed_read_paths.is_empty() {
198            return false;
199        }
200        let Ok(canonical) = std::fs::canonicalize(path) else {
201            return false;
202        };
203        self.allowed_read_paths
204            .iter()
205            .filter_map(|p| std::fs::canonicalize(p).ok())
206            .any(|allowed| canonical.starts_with(&allowed))
207    }
208
209    /// Check if writing to a path is allowed.
210    ///
211    /// Paths are canonicalized before comparison to prevent traversal attacks
212    /// via symlinks or `..` components.
213    ///
214    /// Callers must explicitly list allowed paths: an empty `allowed_write_paths`
215    /// list means no paths are allowed (deny-by-default), not unrestricted access.
216    pub fn can_write(&self, path: &std::path::Path) -> bool {
217        if !self.has_permission(Permission::Write) {
218            return false;
219        }
220        // Empty path list means no paths are explicitly permitted — deny all.
221        if self.allowed_write_paths.is_empty() {
222            return false;
223        }
224        let Ok(canonical) = std::fs::canonicalize(path) else {
225            return false;
226        };
227        self.allowed_write_paths
228            .iter()
229            .filter_map(|p| std::fs::canonicalize(p).ok())
230            .any(|allowed| canonical.starts_with(&allowed))
231    }
232
233    /// Check if executing a command is allowed.
234    pub fn can_execute(&self, command: &str) -> bool {
235        if !self.has_permission(Permission::Execute) {
236            return false;
237        }
238        self.allowed_commands.contains("*") || self.allowed_commands.contains(command)
239    }
240}
241
242/// Sandbox violation that occurred during plugin execution.
243#[derive(Debug, Clone)]
244#[allow(dead_code)]
245pub struct SandboxViolation {
246    /// Type of violation.
247    pub kind: ViolationKind,
248
249    /// Description of what was attempted.
250    pub description: String,
251
252    /// Path involved (if applicable).
253    pub path: Option<PathBuf>,
254
255    /// Command involved (if applicable).
256    pub command: Option<String>,
257}
258
259/// Types of sandbox violations.
260#[derive(Debug, Clone, Copy, PartialEq, Eq)]
261#[allow(dead_code)]
262pub enum ViolationKind {
263    /// Attempted to read from disallowed path.
264    ReadDenied,
265
266    /// Attempted to write to disallowed path.
267    WriteDenied,
268
269    /// Attempted to execute disallowed command.
270    ExecuteDenied,
271
272    /// Attempted network access when not allowed.
273    NetworkDenied,
274
275    /// Attempted to access environment when not allowed.
276    EnvDenied,
277
278    /// Exceeded memory limit.
279    MemoryExceeded,
280
281    /// Exceeded execution timeout.
282    TimeoutExceeded,
283
284    /// Attempted to read file larger than limit.
285    FileTooLarge,
286}
287
288impl std::fmt::Display for SandboxViolation {
289    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
290        match self.kind {
291            ViolationKind::ReadDenied => {
292                write!(f, "Read denied: {}", self.description)
293            }
294            ViolationKind::WriteDenied => {
295                write!(f, "Write denied: {}", self.description)
296            }
297            ViolationKind::ExecuteDenied => {
298                write!(f, "Execute denied: {}", self.description)
299            }
300            ViolationKind::NetworkDenied => {
301                write!(f, "Network access denied")
302            }
303            ViolationKind::EnvDenied => {
304                write!(f, "Environment access denied")
305            }
306            ViolationKind::MemoryExceeded => {
307                write!(f, "Memory limit exceeded")
308            }
309            ViolationKind::TimeoutExceeded => {
310                write!(f, "Execution timeout exceeded")
311            }
312            ViolationKind::FileTooLarge => {
313                write!(f, "File too large: {}", self.description)
314            }
315        }
316    }
317}
318
319impl std::error::Error for SandboxViolation {}