agcodex_execpolicy/
execv_checker.rs1use std::ffi::OsString;
2use std::path::Path;
3use std::path::PathBuf;
4
5use crate::ArgType;
6use crate::Error::CannotCanonicalizePath;
7use crate::Error::CannotCheckRelativePath;
8use crate::Error::ReadablePathNotInReadableFolders;
9use crate::Error::WriteablePathNotInWriteableFolders;
10use crate::ExecCall;
11use crate::MatchedExec;
12use crate::Policy;
13use crate::Result;
14use crate::ValidExec;
15use path_absolutize::*;
16
17macro_rules! check_file_in_folders {
18 ($file:expr, $folders:expr, $error:ident) => {
19 if !$folders.iter().any(|folder| $file.starts_with(folder)) {
20 return Err($error {
21 file: $file.clone(),
22 folders: $folders.to_vec(),
23 });
24 }
25 };
26}
27
28pub struct ExecvChecker {
29 execv_policy: Policy,
30}
31
32impl ExecvChecker {
33 pub const fn new(execv_policy: Policy) -> Self {
34 Self { execv_policy }
35 }
36
37 pub fn r#match(&self, exec_call: &ExecCall) -> Result<MatchedExec> {
38 self.execv_policy.check(exec_call)
39 }
40
41 pub fn check(
44 &self,
45 valid_exec: ValidExec,
46 cwd: &Option<OsString>,
47 readable_folders: &[PathBuf],
48 writeable_folders: &[PathBuf],
49 ) -> Result<String> {
50 for (arg_type, value) in valid_exec
51 .args
52 .into_iter()
53 .map(|arg| (arg.r#type, arg.value))
54 .chain(
55 valid_exec
56 .opts
57 .into_iter()
58 .map(|opt| (opt.r#type, opt.value)),
59 )
60 {
61 match arg_type {
62 ArgType::ReadableFile => {
63 let readable_file = ensure_absolute_path(&value, cwd)?;
64 check_file_in_folders!(
65 readable_file,
66 readable_folders,
67 ReadablePathNotInReadableFolders
68 );
69 }
70 ArgType::WriteableFile => {
71 let writeable_file = ensure_absolute_path(&value, cwd)?;
72 check_file_in_folders!(
73 writeable_file,
74 writeable_folders,
75 WriteablePathNotInWriteableFolders
76 );
77 }
78 ArgType::OpaqueNonFile
79 | ArgType::Unknown
80 | ArgType::PositiveInteger
81 | ArgType::SedCommand
82 | ArgType::Literal(_) => {
83 continue;
84 }
85 }
86 }
87
88 let mut program = valid_exec.program.to_string();
89 for system_path in valid_exec.system_path {
90 if is_executable_file(&system_path) {
91 program = system_path.to_string();
92 break;
93 }
94 }
95
96 Ok(program)
97 }
98}
99
100fn ensure_absolute_path(path: &str, cwd: &Option<OsString>) -> Result<PathBuf> {
101 let file = PathBuf::from(path);
102 let result = if file.is_relative() {
103 match cwd {
104 Some(cwd) => file.absolutize_from(cwd),
105 None => return Err(CannotCheckRelativePath { file }),
106 }
107 } else {
108 file.absolutize()
109 };
110 result
111 .map(|path| path.into_owned())
112 .map_err(|error| CannotCanonicalizePath {
113 file: path.to_string(),
114 error: error.kind(),
115 })
116}
117
118fn is_executable_file(path: &str) -> bool {
119 let file_path = Path::new(path);
120
121 if let Ok(metadata) = std::fs::metadata(file_path) {
122 #[cfg(unix)]
123 {
124 use std::os::unix::fs::PermissionsExt;
125 let permissions = metadata.permissions();
126
127 return metadata.is_file() && (permissions.mode() & 0o111 != 0);
129 }
130
131 #[cfg(windows)]
132 {
133 return metadata.is_file();
135 }
136 }
137
138 false
139}
140
141#[cfg(test)]
142mod tests {
143 use tempfile::TempDir;
144
145 use super::*;
146 use crate::MatchedArg;
147 use crate::PolicyParser;
148
149 fn setup(fake_cp: &Path) -> ExecvChecker {
150 let source = format!(
151 r#"
152define_program(
153program="cp",
154args=[ARG_RFILE, ARG_WFILE],
155system_path=[{fake_cp:?}]
156)
157"#
158 );
159 let parser = PolicyParser::new("#test", &source);
160 let policy = parser.parse().unwrap();
161 ExecvChecker::new(policy)
162 }
163
164 #[test]
165 fn test_check_valid_input_files() -> Result<()> {
166 let temp_dir = TempDir::new().unwrap();
167
168 let fake_cp = temp_dir.path().join("cp");
170 #[cfg(unix)]
171 {
172 use std::os::unix::fs::PermissionsExt;
173
174 let fake_cp_file = std::fs::File::create(&fake_cp).unwrap();
175 let mut permissions = fake_cp_file.metadata().unwrap().permissions();
176 permissions.set_mode(0o755);
177 std::fs::set_permissions(&fake_cp, permissions).unwrap();
178 }
179 #[cfg(windows)]
180 {
181 std::fs::File::create(&fake_cp).unwrap();
182 }
183
184 let root_path = temp_dir.path().to_path_buf();
186 let source_path = root_path.join("source");
187 let dest_path = root_path.join("dest");
188
189 let cp = fake_cp.to_str().unwrap().to_string();
190 let root = root_path.to_str().unwrap().to_string();
191 let source = source_path.to_str().unwrap().to_string();
192 let dest = dest_path.to_str().unwrap().to_string();
193
194 let cwd = Some(root_path.clone().into());
195
196 let checker = setup(&fake_cp);
197 let exec_call = ExecCall {
198 program: "cp".into(),
199 args: vec![source.clone(), dest.clone()],
200 };
201 let valid_exec = match checker.r#match(&exec_call)? {
202 MatchedExec::Match { exec } => exec,
203 unexpected => panic!("Expected a safe exec but got {unexpected:?}"),
204 };
205
206 assert_eq!(
208 checker.check(valid_exec.clone(), &cwd, &[], &[]),
209 Err(ReadablePathNotInReadableFolders {
210 file: source_path.clone(),
211 folders: vec![]
212 }),
213 );
214
215 assert_eq!(
217 checker.check(
218 valid_exec.clone(),
219 &cwd,
220 std::slice::from_ref(&root_path),
221 &[]
222 ),
223 Err(WriteablePathNotInWriteableFolders {
224 file: dest_path.clone(),
225 folders: vec![]
226 }),
227 );
228
229 assert_eq!(
231 checker.check(
232 valid_exec.clone(),
233 &cwd,
234 std::slice::from_ref(&root_path),
235 std::slice::from_ref(&root_path)
236 ),
237 Ok(cp.clone()),
238 );
239
240 let exec_call_folders_as_args = ExecCall {
243 program: "cp".into(),
244 args: vec![root.clone(), root.clone()],
245 };
246 let valid_exec_call_folders_as_args = match checker.r#match(&exec_call_folders_as_args)? {
247 MatchedExec::Match { exec } => exec,
248 _ => panic!("Expected a safe exec"),
249 };
250 assert_eq!(
251 checker.check(
252 valid_exec_call_folders_as_args,
253 &cwd,
254 std::slice::from_ref(&root_path),
255 std::slice::from_ref(&root_path)
256 ),
257 Ok(cp.clone()),
258 );
259
260 let exec_with_parent_of_readable_folder = ValidExec {
262 program: "cp".into(),
263 args: vec![
264 MatchedArg::new(
265 0,
266 ArgType::ReadableFile,
267 root_path.parent().unwrap().to_str().unwrap(),
268 )?,
269 MatchedArg::new(1, ArgType::WriteableFile, &dest)?,
270 ],
271 ..Default::default()
272 };
273 assert_eq!(
274 checker.check(
275 exec_with_parent_of_readable_folder,
276 &cwd,
277 std::slice::from_ref(&root_path),
278 std::slice::from_ref(&dest_path)
279 ),
280 Err(ReadablePathNotInReadableFolders {
281 file: root_path.parent().unwrap().to_path_buf(),
282 folders: vec![root_path.clone()]
283 }),
284 );
285 Ok(())
286 }
287}