Skip to main content

wasm_sandbox/security/
capabilities.rs

1//! Implementation of security capabilities for the sandbox
2
3use std::collections::HashSet;
4use std::path::{Path, PathBuf};
5use std::net::{IpAddr, SocketAddr};
6
7use crate::error::{Error, Result, SecurityContext};
8use crate::security::{
9    NetworkCapability, FilesystemCapability, 
10    EnvironmentCapability, ProcessCapability, TimeCapability, RandomCapability
11};
12
13/// Capability verification helper
14pub trait CapabilityVerifier {
15    /// Verify that an operation is allowed by the capabilities
16    fn verify(&self, operation: &str, params: &[&str]) -> Result<()>;
17}
18
19/// Network capability verifier
20pub struct NetworkVerifier {
21    capability: NetworkCapability,
22}
23
24impl NetworkVerifier {
25    /// Create a new network verifier
26    pub fn new(capability: NetworkCapability) -> Self {
27        Self { capability }
28    }
29    
30    /// Check if a host is allowed
31    pub fn is_host_allowed(&self, host: &str, port: u16, secure: bool) -> bool {
32        match &self.capability {
33            NetworkCapability::None => false,
34            NetworkCapability::Loopback => {
35                // Check if host is localhost or 127.0.0.1
36                host == "localhost" || host == "127.0.0.1" || host == "::1"
37            },
38            NetworkCapability::AllowedHosts(hosts) => {
39                // Check if host is in the allowed hosts list
40                hosts.iter().any(|h| {
41                    // Check host
42                    if h.host != host {
43                        return false;
44                    }
45                    
46                    // Check port if specified
47                    if let Some(port_range) = &h.ports {
48                        if !port_range.contains(port) {
49                            return false;
50                        }
51                    }
52                    
53                    // Check secure flag
54                    if !h.secure && secure {
55                        return false;
56                    }
57                    
58                    true
59                })
60            },
61            NetworkCapability::AllowedPorts(ports) => {
62                // Check if port is in any allowed port range
63                ports.iter().any(|r| r.contains(port))
64            },
65            NetworkCapability::Full => true,
66        }
67    }
68    
69    /// Check if an IP address is allowed
70    pub fn is_ip_allowed(&self, ip: IpAddr, port: u16) -> bool {
71        match &self.capability {
72            NetworkCapability::None => false,
73            NetworkCapability::Loopback => {
74                // Check if IP is localhost
75                match ip {
76                    IpAddr::V4(addr) => addr.is_loopback(),
77                    IpAddr::V6(addr) => addr.is_loopback(),
78                }
79            },
80            NetworkCapability::AllowedHosts(_hosts) => {
81                // Not implemented: would need to resolve hosts to IPs
82                false
83            },
84            NetworkCapability::AllowedPorts(ports) => {
85                // Check if port is in any allowed port range
86                ports.iter().any(|r| r.contains(port))
87            },
88            NetworkCapability::Full => true,
89        }
90    }
91    
92    /// Check if a socket address is allowed
93    pub fn is_socket_allowed(&self, socket: SocketAddr) -> bool {
94        self.is_ip_allowed(socket.ip(), socket.port())
95    }
96}
97
98impl CapabilityVerifier for NetworkVerifier {
99    fn verify(&self, operation: &str, params: &[&str]) -> Result<()> {
100        match operation {
101            "connect" => {
102                if params.len() < 2 {
103                    return Err(Error::Capability { message: "Missing host and port for connect".to_string() });
104                }
105                
106                let host = params[0];
107                let port = params[1].parse::<u16>().map_err(|_| {
108                    Error::Capability { message: format!("Invalid port: {}", params[1]) }
109                })?;
110                
111                let secure = params.get(2).map(|s| *s == "secure").unwrap_or(false);
112                
113                if !self.is_host_allowed(host, port, secure) {
114                    return Err(Error::SecurityViolation {
115                        violation: format!("Network access denied to {}:{}", host, port),
116                        instance_id: None,
117                        context: create_security_context("connect", "network.connect", &[]),
118                    });
119                }
120            }
121            "bind" => {
122                if params.len() < 2 {
123                    return Err(Error::Capability { message: "Missing host and port for bind".to_string() });
124                }
125                
126                let host = params[0];
127                let port = params[1].parse::<u16>().map_err(|_| {
128                    Error::Capability { message: format!("Invalid port: {}", params[1]) }
129                })?;
130                
131                if !self.is_host_allowed(host, port, false) {
132                    return Err(Error::SecurityViolation {
133                        violation: format!("Network binding denied to {}:{}", host, port),
134                        instance_id: None,
135                        context: create_security_context("bind", "network.bind", &[]),
136                    });
137                }
138            }
139            "listen" => {
140                if params.len() < 1 {
141                    return Err(Error::Capability { message: "Missing port for listen".to_string() });
142                }
143                
144                let port = params[0].parse::<u16>().map_err(|_| {
145                    Error::Capability { message: format!("Invalid port: {}", params[0]) }
146                })?;
147                
148                // For listen, we check if the loopback address is allowed with this port
149                if !self.is_host_allowed("127.0.0.1", port, false) {
150                    return Err(Error::SecurityViolation {
151                        violation: format!("Network listening denied on port {}", port),
152                        instance_id: None,
153                        context: create_security_context("listen", "network.listen", &[]),
154                    });
155                }
156            }
157            _ => {
158                return Err(Error::Capability { message: format!("Unknown network operation: {}", operation) });
159            }
160        }
161        
162        Ok(())
163    }
164}
165
166/// Filesystem capability verifier
167pub struct FilesystemVerifier {
168    capability: FilesystemCapability,
169    normalized_readable: HashSet<PathBuf>,
170    normalized_writable: HashSet<PathBuf>,
171}
172
173impl FilesystemVerifier {
174    /// Create a new filesystem verifier
175    pub fn new(capability: FilesystemCapability) -> Self {
176        // Normalize paths for better comparison
177        let normalized_readable = capability.readable_dirs
178            .iter()
179            .filter_map(|p| std::fs::canonicalize(p).ok())
180            .collect();
181            
182        let normalized_writable = capability.writable_dirs
183            .iter()
184            .filter_map(|p| std::fs::canonicalize(p).ok())
185            .collect();
186        
187        Self { 
188            capability,
189            normalized_readable,
190            normalized_writable,
191        }
192    }
193    
194    /// Check if a path is readable
195    pub fn is_readable(&self, path: &Path) -> bool {
196        // Try to canonicalize the path
197        let canon_path = match std::fs::canonicalize(path) {
198            Ok(p) => p,
199            Err(_) => return false, // Path doesn't exist or other error
200        };
201        
202        // Check if the path is in any readable directory
203        for dir in &self.normalized_readable {
204            if is_path_within(dir, &canon_path) {
205                return true;
206            }
207        }
208        
209        // Also check if it's writable (writable implies readable)
210        self.is_writable(path)
211    }
212    
213    /// Check if a path is writable
214    pub fn is_writable(&self, path: &Path) -> bool {
215        // Try to canonicalize the path
216        let canon_path = match std::fs::canonicalize(path) {
217            Ok(p) => p,
218            Err(_) => {
219                // If path doesn't exist, check its parent directory
220                if let Some(parent) = path.parent() {
221                    match std::fs::canonicalize(parent) {
222                        Ok(p) => p,
223                        Err(_) => return false, // Parent doesn't exist
224                    }
225                } else {
226                    return false; // No parent (root)
227                }
228            }
229        };
230        
231        // Check if the path is in any writable directory
232        for dir in &self.normalized_writable {
233            if is_path_within(dir, &canon_path) {
234                return true;
235            }
236        }
237        
238        false
239    }
240    
241    /// Check if file creation is allowed
242    pub fn can_create(&self) -> bool {
243        self.capability.allow_create
244    }
245    
246    /// Check if file deletion is allowed
247    pub fn can_delete(&self) -> bool {
248        self.capability.allow_delete
249    }
250    
251    /// Check if a file size is within limits
252    pub fn is_size_allowed(&self, size: u64) -> bool {
253        match self.capability.max_file_size {
254            Some(limit) => size <= limit,
255            None => true,
256        }
257    }
258}
259
260impl CapabilityVerifier for FilesystemVerifier {
261    fn verify(&self, operation: &str, params: &[&str]) -> Result<()> {
262        match operation {
263            "open" | "read" => {
264                if params.is_empty() {
265                    return Err(Error::Capability { message: "Missing path for open/read".to_string() });
266                }
267                
268                let path = Path::new(params[0]);
269                if !self.is_readable(path) {
270                    return Err(Error::SecurityViolation {
271                        violation: format!("File read access denied: {}", path.display()),
272                        instance_id: None,
273                        context: create_security_context("read", "filesystem.read", &[]),
274                    });
275                }
276            }
277            "write" | "append" => {
278                if params.is_empty() {
279                    return Err(Error::Capability { message: "Missing path for write/append".to_string() });
280                }
281                
282                let path = Path::new(params[0]);
283                if !self.is_writable(path) {
284                    return Err(Error::SecurityViolation {
285                        violation: format!("File write access denied: {}", path.display()),
286                        instance_id: None,
287                        context: create_security_context("write", "filesystem.write", &[]),
288                    });
289                }
290                
291                // Check size limit if provided
292                if params.len() > 1 {
293                    let size = params[1].parse::<u64>().map_err(|_| {
294                        Error::Capability { message: format!("Invalid size: {}", params[1]) }
295                    })?;
296                    
297                    if !self.is_size_allowed(size) {
298                        return Err(Error::ResourceLimit {
299                            message: format!("File size limit exceeded: {}", size)
300                        });
301                    }
302                }
303            }
304            "create" => {
305                if params.is_empty() {
306                    return Err(Error::Capability { message: "Missing path for create".to_string() });
307                }
308                
309                if !self.can_create() {
310                    return Err(Error::SecurityViolation {
311                        violation: "File creation is not allowed".to_string(),
312                        instance_id: None,
313                        context: create_security_context("create", "filesystem.create", &[]),
314                    });
315                }
316                
317                let path = Path::new(params[0]);
318                if !self.is_writable(path) {
319                    return Err(Error::SecurityViolation {
320                        violation: format!("File creation access denied: {}", path.display()),
321                        instance_id: None,
322                        context: create_security_context("create", "filesystem.create", &[]),
323                    });
324                }
325            }
326            "delete" | "remove" => {
327                if params.is_empty() {
328                    return Err(Error::Capability { message: "Missing path for delete/remove".to_string() });
329                }
330                
331                if !self.can_delete() {
332                    return Err(Error::SecurityViolation {
333                        violation: "File deletion is not allowed".to_string(),
334                        instance_id: None,
335                        context: create_security_context("delete", "filesystem.delete", &[]),
336                    });
337                }
338                
339                let path = Path::new(params[0]);
340                if !self.is_writable(path) {
341                    return Err(Error::SecurityViolation {
342                        violation: format!("File deletion access denied: {}", path.display()),
343                        instance_id: None,
344                        context: create_security_context("delete", "filesystem.delete", &[]),
345                    });
346                }
347            }
348            _ => {
349                return Err(Error::Capability { message: format!("Unknown filesystem operation: {}", operation) });
350            }
351        }
352        
353        Ok(())
354    }
355}
356
357/// Helper function to check if a path is within a directory
358fn is_path_within(dir: &Path, path: &Path) -> bool {
359    let dir_str = dir.to_string_lossy();
360    let path_str = path.to_string_lossy();
361    
362    // Check if path starts with dir (and there's either an exact match or a path separator after)
363    if path_str == dir_str {
364        return true;
365    }
366    
367    path_str.starts_with(&format!("{}{}", dir_str, std::path::MAIN_SEPARATOR))
368}
369
370/// Environment capability verifier
371pub struct EnvironmentVerifier {
372    capability: EnvironmentCapability,
373}
374
375impl EnvironmentVerifier {
376    /// Create a new environment verifier
377    pub fn new(capability: EnvironmentCapability) -> Self {
378        Self { capability }
379    }
380    
381    /// Check if a variable is allowed
382    pub fn is_var_allowed(&self, var: &str) -> bool {
383        match &self.capability {
384            EnvironmentCapability::None => false,
385            EnvironmentCapability::Allowlist(allowed) => allowed.iter().any(|v| v == var),
386            EnvironmentCapability::Denylist(denied) => !denied.iter().any(|v| v == var),
387            EnvironmentCapability::Full => true,
388        }
389    }
390}
391
392impl CapabilityVerifier for EnvironmentVerifier {
393    fn verify(&self, operation: &str, params: &[&str]) -> Result<()> {
394        match operation {
395            "get" | "set" => {
396                if params.is_empty() {
397                    return Err(Error::Capability { message: "Missing variable name".to_string() });
398                }
399                
400                let var = params[0];
401                if !self.is_var_allowed(var) {
402                    return Err(Error::SecurityViolation {
403                        violation: format!("Environment variable access denied: {}", var),
404                        instance_id: None,
405                        context: create_security_context("get", "environment.get", &[]),
406                    });
407                }
408                
409                // For "set", additionally check if we're in Full mode
410                if operation == "set" && !matches!(self.capability, EnvironmentCapability::Full) {
411                    return Err(Error::SecurityViolation {
412                        violation: format!("Setting environment variables is not allowed: {}", var),
413                        instance_id: None,
414                        context: create_security_context("set", "environment.set", &[]),
415                    });
416                }
417            }
418            _ => {
419                return Err(Error::Capability { message: format!("Unknown environment operation: {}", operation) });
420            }
421        }
422        
423        Ok(())
424    }
425}
426
427/// Process capability verifier
428pub struct ProcessVerifier {
429    capability: ProcessCapability,
430}
431
432impl ProcessVerifier {
433    /// Create a new process verifier
434    pub fn new(capability: ProcessCapability) -> Self {
435        Self { capability }
436    }
437    
438    /// Check if a command is allowed to be executed
439    pub fn is_command_allowed(&self, command: &str) -> bool {
440        match &self.capability {
441            ProcessCapability::None => false,
442            ProcessCapability::AllowedCommands(allowed) => {
443                // Check if the command matches any allowed commands
444                allowed.iter().any(|cmd| {
445                    // Exact match
446                    if cmd == command {
447                        return true;
448                    }
449                    
450                    // Wildcard match
451                    if cmd.ends_with("*") {
452                        let prefix = &cmd[..cmd.len() - 1];
453                        return command.starts_with(prefix);
454                    }
455                    
456                    false
457                })
458            }
459            ProcessCapability::Full => true,
460        }
461    }
462}
463
464impl CapabilityVerifier for ProcessVerifier {
465    fn verify(&self, operation: &str, params: &[&str]) -> Result<()> {
466        match operation {
467            "exec" | "spawn" => {
468                if params.is_empty() {
469                    return Err(Error::Capability { message: "Missing command".to_string() });
470                }
471                
472                let command = params[0];
473                if !self.is_command_allowed(command) {
474                    return Err(Error::SecurityViolation {
475                        violation: format!("Process execution denied: {}", command),
476                        instance_id: None,
477                        context: create_security_context("exec", "process.exec", &[]),
478                    });
479                }
480            }
481            _ => {
482                return Err(Error::Capability { message: format!("Unknown process operation: {}", operation) });
483            }
484        }
485        
486        Ok(())
487    }
488}
489
490/// Time capability verifier
491pub struct TimeVerifier {
492    capability: TimeCapability,
493}
494
495impl TimeVerifier {
496    /// Create a new time verifier
497    pub fn new(capability: TimeCapability) -> Self {
498        Self { capability }
499    }
500    
501    /// Check if setting time is allowed
502    pub fn can_set_time(&self) -> bool {
503        matches!(self.capability, TimeCapability::Full)
504    }
505}
506
507impl CapabilityVerifier for TimeVerifier {
508    fn verify(&self, operation: &str, _params: &[&str]) -> Result<()> {
509        match operation {
510            "get" => {
511                // Reading time is always allowed
512                Ok(())
513            }
514            "set" => {
515                if !self.can_set_time() {
516                    return Err(Error::SecurityViolation {
517                        violation: "Setting time is not allowed".to_string(),
518                        instance_id: None,
519                        context: create_security_context("set", "time.set", &[]),
520                    });
521                }
522                Ok(())
523            }
524            _ => {
525                Err(Error::Capability { message: format!("Unknown time operation: {}", operation) })
526            }
527        }
528    }
529}
530
531/// Random capability verifier
532pub struct RandomVerifier {
533    capability: RandomCapability,
534}
535
536impl RandomVerifier {
537    /// Create a new random verifier
538    pub fn new(capability: RandomCapability) -> Self {
539        Self { capability }
540    }
541    
542    /// Check if secure random generation is allowed
543    pub fn can_secure_random(&self) -> bool {
544        matches!(self.capability, RandomCapability::Full)
545    }
546    
547    /// Check if any random generation is allowed
548    pub fn can_pseudo_random(&self) -> bool {
549        !matches!(self.capability, RandomCapability::None)
550    }
551}
552
553impl CapabilityVerifier for RandomVerifier {
554    fn verify(&self, operation: &str, _params: &[&str]) -> Result<()> {
555        match operation {
556            "pseudo" => {
557                if !self.can_pseudo_random() {
558                    return Err(Error::SecurityViolation {
559                        violation: "Pseudo-random generation is not allowed".to_string(),
560                        instance_id: None,
561                        context: create_security_context("pseudo", "random.pseudo", &[]),
562                    });
563                }
564                Ok(())
565            }
566            "secure" => {
567                if !self.can_secure_random() {
568                    return Err(Error::SecurityViolation {
569                        violation: "Secure random generation is not allowed".to_string(),
570                        instance_id: None,
571                        context: create_security_context("secure", "random.secure", &[]),
572                    });
573                }
574                Ok(())
575            }
576            _ => {
577                Err(Error::Capability { message: format!("Unknown random operation: {}", operation) })
578            }
579        }
580    }
581}
582
583/// Helper function to create SecurityContext for capability violations
584fn create_security_context(operation: &str, required_capability: &str, available_capabilities: &[&str]) -> SecurityContext {
585    SecurityContext {
586        attempted_operation: operation.to_string(),
587        required_capability: required_capability.to_string(),
588        available_capabilities: available_capabilities.iter().map(|s| s.to_string()).collect(),
589    }
590}
591
592/// Central capability manager that combines all verifiers
593pub struct CapabilityManager {
594    /// Network verifier
595    pub network: NetworkVerifier,
596    
597    /// Filesystem verifier
598    pub filesystem: FilesystemVerifier,
599    
600    /// Environment verifier
601    pub environment: EnvironmentVerifier,
602    
603    /// Process verifier
604    pub process: ProcessVerifier,
605    
606    /// Time verifier
607    pub time: TimeVerifier,
608    
609    /// Random verifier
610    pub random: RandomVerifier,
611}
612
613impl CapabilityManager {
614    /// Create a new capability manager
615    pub fn new(
616        network: NetworkCapability,
617        filesystem: FilesystemCapability,
618        environment: EnvironmentCapability,
619        process: ProcessCapability,
620        time: TimeCapability,
621        random: RandomCapability,
622    ) -> Self {
623        Self {
624            network: NetworkVerifier::new(network),
625            filesystem: FilesystemVerifier::new(filesystem),
626            environment: EnvironmentVerifier::new(environment),
627            process: ProcessVerifier::new(process),
628            time: TimeVerifier::new(time),
629            random: RandomVerifier::new(random),
630        }
631    }
632    
633    /// Check if an operation is allowed based on its capability domain
634    pub fn verify(&self, domain: &str, operation: &str, params: &[&str]) -> Result<()> {
635        match domain {
636            "network" => self.network.verify(operation, params),
637            "filesystem" | "fs" => self.filesystem.verify(operation, params),
638            "environment" | "env" => self.environment.verify(operation, params),
639            "process" | "proc" => self.process.verify(operation, params),
640            "time" => self.time.verify(operation, params),
641            "random" | "rand" => self.random.verify(operation, params),
642            _ => Err(Error::Capability { message: format!("Unknown capability domain: {}", domain) }),
643        }
644    }
645}