Skip to main content

polykit_core/
command_validator.rs

1//! Command validation for security.
2
3use crate::error::{Error, Result};
4
5/// Validates shell commands before execution to prevent injection attacks.
6///
7/// This validator checks for dangerous patterns that could allow command
8/// injection or arbitrary code execution.
9pub struct CommandValidator {
10    allow_shell: bool,
11}
12
13impl Default for CommandValidator {
14    fn default() -> Self {
15        Self { allow_shell: true }
16    }
17}
18
19impl CommandValidator {
20    /// Creates a new command validator.
21    pub fn new() -> Self {
22        Self::default()
23    }
24
25    /// Creates a validator that disallows shell features.
26    pub fn strict() -> Self {
27        Self { allow_shell: false }
28    }
29
30    /// Validates a command string before execution.
31    ///
32    /// # Errors
33    ///
34    /// Returns an error if the command contains dangerous patterns.
35    pub fn validate(&self, command: &str) -> Result<()> {
36        if command.trim().is_empty() {
37            return Err(Error::TaskExecution {
38                package: "unknown".to_string(),
39                task: "unknown".to_string(),
40                message: "Command cannot be empty".to_string(),
41            });
42        }
43
44        if command.len() > 10_000 {
45            return Err(Error::TaskExecution {
46                package: "unknown".to_string(),
47                task: "unknown".to_string(),
48                message: "Command exceeds maximum length of 10,000 characters".to_string(),
49            });
50        }
51
52        if command.contains('\0') {
53            return Err(Error::TaskExecution {
54                package: "unknown".to_string(),
55                task: "unknown".to_string(),
56                message: "Command contains null bytes".to_string(),
57            });
58        }
59
60        if command.contains("\r\n") || command.contains('\n') {
61            let trimmed = command.trim();
62            if trimmed.contains('\n') {
63                return Err(Error::TaskExecution {
64                    package: "unknown".to_string(),
65                    task: "unknown".to_string(),
66                    message: "Command contains embedded newlines".to_string(),
67                });
68            }
69        }
70
71        if !self.allow_shell
72            && (command.contains(';')
73                || command.contains("&&")
74                || command.contains("||")
75                || command.contains('|')
76                || command.contains('`')
77                || command.contains('$'))
78        {
79            return Err(Error::TaskExecution {
80                package: "unknown".to_string(),
81                task: "unknown".to_string(),
82                message: "Command contains shell features that are not allowed in strict mode"
83                    .to_string(),
84            });
85        }
86
87        Ok(())
88    }
89
90    /// Validates an identifier such as a package name or task name.
91    ///
92    /// Identifiers must be alphanumeric with hyphens, underscores, or dots only.
93    pub fn validate_identifier(identifier: &str, context: &str) -> Result<()> {
94        if identifier.is_empty() {
95            return Err(Error::InvalidPackageName(format!(
96                "{} cannot be empty",
97                context
98            )));
99        }
100
101        if identifier.len() > 255 {
102            return Err(Error::InvalidPackageName(format!(
103                "{} exceeds maximum length of 255 characters",
104                context
105            )));
106        }
107
108        if identifier.starts_with('.') || identifier.starts_with('-') {
109            return Err(Error::InvalidPackageName(format!(
110                "{} cannot start with '.' or '-'",
111                context
112            )));
113        }
114
115        if identifier.contains("..") {
116            return Err(Error::InvalidPackageName(format!(
117                "{} cannot contain '..'",
118                context
119            )));
120        }
121
122        if identifier.contains('/') || identifier.contains('\\') {
123            return Err(Error::InvalidPackageName(format!(
124                "{} cannot contain path separators",
125                context
126            )));
127        }
128
129        for ch in identifier.chars() {
130            if !ch.is_alphanumeric() && ch != '-' && ch != '_' && ch != '.' && ch != '@' {
131                return Err(Error::InvalidPackageName(format!(
132                    "{} contains invalid character: '{}'",
133                    context, ch
134                )));
135            }
136        }
137
138        Ok(())
139    }
140}