agcodex_execpolicy/
execv_checker.rs

1use 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    /// The caller is responsible for ensuring readable_folders and
42    /// writeable_folders are in canonical form.
43    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            // Check if the file is executable (by checking the executable bit for the owner)
128            return metadata.is_file() && (permissions.mode() & 0o111 != 0);
129        }
130
131        #[cfg(windows)]
132        {
133            // TODO(mbolin): Check against PATHEXT environment variable.
134            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        // Create an executable file that can be used with the system_path arg.
169        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        // Create root_path and reference to files under the root.
185        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        // No readable or writeable folders specified.
207        assert_eq!(
208            checker.check(valid_exec.clone(), &cwd, &[], &[]),
209            Err(ReadablePathNotInReadableFolders {
210                file: source_path.clone(),
211                folders: vec![]
212            }),
213        );
214
215        // Only readable folders specified.
216        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        // Both readable and writeable folders specified.
230        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        // Args are the readable and writeable folders, not files within the
241        // folders.
242        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        // Specify a parent of a readable folder as input.
261        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}