Skip to main content

fs_mistrust/
file_access.rs

1//! Functionality for opening files while verifying their permissions.
2
3use std::{
4    borrow::Cow,
5    fs::{File, OpenOptions},
6    io::{Read as _, Write},
7    path::{Path, PathBuf},
8};
9
10#[cfg(unix)]
11use std::os::unix::fs::OpenOptionsExt as _;
12
13use crate::{CheckedDir, Error, Result, Verifier, dir::FullPathCheck, walk::PathType};
14
15/// Helper object for accessing a file on disk while checking the necessary permissions.
16///
17/// You can use a `FileAccess` when you want to read or write a file,
18/// while making sure that the file obeys the permissions rules of
19/// an associated [`CheckedDir`] or [`Verifier`].
20///
21/// `FileAccess` is a separate type from `CheckedDir` and `Verifier`
22/// so that you can set options to control the behavior of how files are opened.
23///
24/// Note: When we refer to a path _"obeying the constraints"_ of this `FileAccess`,
25/// we mean:
26/// * If the `FileAccess` wraps a `CheckedDir`, the requirement that it is a relative path
27///   containing no ".." elements,
28///   or other elements that would take it outside the `CheckedDir`.
29/// * If the `FileAccess` wraps a `Verifier`, there are no requirements.
30pub struct FileAccess<'a> {
31    /// Validator object that we use for checking file permissions.
32    pub(crate) inner: Inner<'a>,
33    /// If set, we create files with this mode.
34    #[cfg(unix)]
35    create_with_mode: Option<u32>,
36    /// If set, we follow final-position symlinks in provided paths.
37    follow_final_links: bool,
38}
39
40/// Inner object for checking file permissions.
41pub(crate) enum Inner<'a> {
42    /// A CheckedDir backing this FileAccess.
43    CheckedDir(&'a CheckedDir),
44    /// A Verifier backing this FileAccess.
45    Verifier(Verifier<'a>),
46}
47
48impl<'a> FileAccess<'a> {
49    /// Create a new `FileAccess` to access files within CheckedDir,
50    /// using default options.
51    pub(crate) fn from_checked_dir(checked_dir: &'a CheckedDir) -> Self {
52        Self::from_inner(Inner::CheckedDir(checked_dir))
53    }
54    /// Create a new `FileAccess` to access files anywhere on the filesystem,
55    /// using default options.
56    pub(crate) fn from_verifier(verifier: Verifier<'a>) -> Self {
57        Self::from_inner(Inner::Verifier(verifier))
58    }
59    /// Create a new `FileAccess` from `inner`,
60    /// using default options.
61    fn from_inner(inner: Inner<'a>) -> Self {
62        Self {
63            inner,
64            #[cfg(unix)]
65            create_with_mode: None,
66            follow_final_links: false,
67        }
68    }
69    /// Check path constraints on `path` and verify its permissions
70    /// (or the permissions of its parent) according to `check_type`
71    fn verified_full_path(&self, path: &Path, check_type: FullPathCheck) -> Result<PathBuf> {
72        match &self.inner {
73            Inner::CheckedDir(cd) => cd.verified_full_path(path, check_type),
74            Inner::Verifier(v) => {
75                let to_verify = match check_type {
76                    FullPathCheck::CheckPath => path,
77                    FullPathCheck::CheckParent => path.parent().unwrap_or(path),
78                };
79                v.check(to_verify)?;
80                Ok(path.into())
81            }
82        }
83    }
84    /// Return a `Verifier` to use for checking permissions.
85    fn verifier(&self) -> crate::Verifier {
86        match &self.inner {
87            Inner::CheckedDir(cd) => cd.verifier(),
88            Inner::Verifier(v) => v.clone(),
89        }
90    }
91    /// Return the location of `path` relative to this verifier.
92    ///
93    /// Fails if `path` does not [obey the constraints](FileAccess) of this `FileAccess`,
94    /// but does not do any permissions checking.
95    fn location_unverified<'b>(&self, path: &'b Path) -> Result<Cow<'b, Path>> {
96        Ok(match self.inner {
97            Inner::CheckedDir(cd) => cd.join(path)?.into(),
98            Inner::Verifier(_) => path.into(),
99        })
100    }
101
102    /// Configure this FileAccess: when used to create a file,
103    /// that file will be created with the provided unix permissions.
104    ///
105    /// If this option is not set, newly created files have mode 0600.
106    #[cfg_attr(not(unix), expect(unused_mut))]
107    #[cfg_attr(not(unix), expect(unused_variables))]
108    pub fn create_with_mode(mut self, mode: u32) -> Self {
109        #[cfg(unix)]
110        {
111            self.create_with_mode = Some(mode);
112        }
113        self
114    }
115
116    /// Configure this FileAccess: if the file to be accessed is a symlink,
117    /// and this is set to true, we will follow that symlink when creating or reading the file.
118    ///
119    /// By default, this option is false.
120    ///
121    /// Note that if you use this option with a `CheckedDir`,
122    /// it can read or write a file outside of the `CheckedDir`,
123    /// which might not be what you wanted.
124    ///
125    /// This option does not affect the handling of links that are _not_
126    /// in the final position of the path.
127    ///
128    /// This option does not disable the regular `fs-mistrust` checks:
129    /// we still ensure that the link's target, and its location, are not
130    /// modifiable by an untrusted user.
131    pub fn follow_final_links(mut self, follow: bool) -> Self {
132        self.follow_final_links = follow;
133        self
134    }
135
136    /// Open a file relative to this `FileAccess`, using a set of [`OpenOptions`].
137    ///
138    /// `path` must be a path to the new file, [obeying the constraints](FileAccess) of this `FileAccess`.
139    /// We check, but do not create, the file's parent directories.
140    /// We check the file's permissions after opening it.
141    ///
142    /// If the file is created (and this is a unix-like operating system), we
143    /// always create it with a mode based on [`create_with_mode()`](Self::create_with_mode),
144    /// regardless of any mode set in `options`.
145    /// If `create_with_mode()` wasn't called, we create the file with mode 600.
146    //
147    // Note: This function, and others, take ownership of `self`, to prevent people from storing and
148    // reusing FileAccess objects.  We're avoiding that because we don't want people confusing
149    // FileAccess objects created with CheckedDir and Verifier.
150    pub fn open<P: AsRef<Path>>(self, path: P, options: &OpenOptions) -> Result<File> {
151        self.open_internal(path.as_ref(), options)
152    }
153
154    /// As `open`, but take `self` by reference.  For internal use.
155    fn open_internal(&self, path: &Path, options: &OpenOptions) -> Result<File> {
156        let follow_links = self.follow_final_links;
157
158        // If we're following links, then we want to look at the whole path,
159        // since the final element might be a link.  If so, we need to look at
160        // where it is linking to, and validate that as well.
161        let check_type = if follow_links {
162            FullPathCheck::CheckPath
163        } else {
164            FullPathCheck::CheckParent
165        };
166
167        let path = match self.verified_full_path(path.as_ref(), check_type) {
168            Ok(path) => path.into(),
169            // We tolerate a not-found error if we're following links:
170            // - If the final element of the path is what wasn't found, then we might create it
171            //   ourselves when we open it.
172            // - If an earlier element of the path wasn't found, we will get a second NotFound error
173            //   when we try to open the file, which is okay.
174            Err(Error::NotFound(_)) if follow_links => self.location_unverified(path.as_ref())?,
175            Err(e) => return Err(e),
176        };
177
178        #[allow(unused_mut)]
179        let mut options = options.clone();
180
181        #[cfg(unix)]
182        {
183            let create_mode = self.create_with_mode.unwrap_or(0o600);
184            options.mode(create_mode);
185            // Don't follow symlinks out of a secure directory.
186            if !follow_links {
187                options.custom_flags(libc::O_NOFOLLOW);
188            }
189        }
190
191        let file = options
192            .open(&path)
193            .map_err(|e| Error::io(e, path.as_ref(), "open file"))?;
194        let meta = file
195            .metadata()
196            .map_err(|e| Error::inspecting(e, path.as_ref()))?;
197
198        if let Some(error) = self
199            .verifier()
200            .check_one(path.as_ref(), PathType::Content, &meta)
201            .into_iter()
202            .next()
203        {
204            Err(error)
205        } else {
206            Ok(file)
207        }
208    }
209
210    /// Read the contents of the file at `path` relative to this `FileAccess`, as a
211    /// String, if possible.
212    ///
213    /// Return an error if `path` is absent, if its permissions are incorrect,
214    /// if it does not [obey the constraints](FileAccess) of this `FileAccess`,
215    /// or if its contents are not UTF-8.
216    pub fn read_to_string<P: AsRef<Path>>(self, path: P) -> Result<String> {
217        let path = path.as_ref();
218        let mut file = self.open(path, OpenOptions::new().read(true))?;
219        let mut result = String::new();
220        file.read_to_string(&mut result)
221            .map_err(|e| Error::io(e, path, "read file"))?;
222        Ok(result)
223    }
224
225    /// Read the contents of the file at `path` relative to this `FileAccess`, as a
226    /// vector of bytes, if possible.
227    ///
228    /// Return an error if `path` is absent, if its permissions are incorrect,
229    /// or if it does not [obey the constraints](FileAccess) of this `FileAccess`.
230    pub fn read<P: AsRef<Path>>(self, path: P) -> Result<Vec<u8>> {
231        let path = path.as_ref();
232        let mut file = self.open(path, OpenOptions::new().read(true))?;
233        let mut result = Vec::new();
234        file.read_to_end(&mut result)
235            .map_err(|e| Error::io(e, path, "read file"))?;
236        Ok(result)
237    }
238
239    /// Store `contents` into the file located at `path` relative to this `FileAccess`.
240    ///
241    /// We won't write to `path` directly: instead, we'll write to a temporary
242    /// file in the same directory as `path`, and then replace `path` with that
243    /// temporary file if we were successful.  (This isn't truly atomic on all
244    /// file systems, but it's closer than many alternatives.)
245    ///
246    /// # Limitations
247    ///
248    /// This function will clobber any existing files with the same name as
249    /// `path` but with the extension `tmp`.  (That is, if you are writing to
250    /// "foo.txt", it will replace "foo.tmp" in the same directory.)
251    ///
252    /// This function may give incorrect behavior if multiple threads or
253    /// processes are writing to the same file at the same time: it is the
254    /// programmer's responsibility to use appropriate locking to avoid this.
255    pub fn write_and_replace<P: AsRef<Path>, C: AsRef<[u8]>>(
256        self,
257        path: P,
258        contents: C,
259    ) -> Result<()> {
260        let path = path.as_ref();
261        let final_path = self.verified_full_path(path, FullPathCheck::CheckParent)?;
262
263        let tmp_name = path.with_extension("tmp");
264        // We remove the temporary file before opening it, if it's present: otherwise it _might_ be
265        // a symlink to somewhere silly.
266        let _ignore = std::fs::remove_file(&tmp_name);
267
268        // TODO: The parent directory  verification performed by "open" here is redundant with that done in
269        // `verified_full_path` above.
270        let mut tmp_file = self.open_internal(
271            &tmp_name,
272            OpenOptions::new().create(true).truncate(true).write(true),
273        )?;
274
275        // Write the data.
276        tmp_file
277            .write_all(contents.as_ref())
278            .map_err(|e| Error::io(e, &tmp_name, "write to file"))?;
279        // Flush and close.
280        drop(tmp_file);
281
282        // Replace the old file.
283        std::fs::rename(
284            // It's okay to use location_unverified here, since we already verified it when we
285            // called `open`.
286            self.location_unverified(tmp_name.as_path())?,
287            final_path,
288        )
289        .map_err(|e| Error::io(e, path, "replace file"))?;
290        Ok(())
291    }
292}
293
294#[cfg(test)]
295mod test {
296    // @@ begin test lint list maintained by maint/add_warning @@
297    #![allow(clippy::bool_assert_comparison)]
298    #![allow(clippy::clone_on_copy)]
299    #![allow(clippy::dbg_macro)]
300    #![allow(clippy::mixed_attributes_style)]
301    #![allow(clippy::print_stderr)]
302    #![allow(clippy::print_stdout)]
303    #![allow(clippy::single_char_pattern)]
304    #![allow(clippy::unwrap_used)]
305    #![allow(clippy::unchecked_time_subtraction)]
306    #![allow(clippy::useless_vec)]
307    #![allow(clippy::needless_pass_by_value)]
308    #![allow(clippy::string_slice)] // See arti#2571
309    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
310
311    #[cfg(unix)]
312    use std::fs;
313
314    use super::*;
315    use crate::{Mistrust, testing::Dir};
316
317    #[test]
318    fn create_public_in_checked_dir() {
319        let d = Dir::new();
320        d.dir("a");
321
322        d.chmod("a", 0o700);
323
324        let m = Mistrust::builder()
325            .ignore_prefix(d.canonical_root())
326            .build()
327            .unwrap();
328        let checked = m.verifier().secure_dir(d.path("a")).unwrap();
329
330        {
331            let mut f = checked
332                .file_access()
333                .open(
334                    "private-1.txt",
335                    OpenOptions::new().write(true).create_new(true),
336                )
337                .unwrap();
338            f.write_all(b"Hello world\n").unwrap();
339
340            checked
341                .file_access()
342                .write_and_replace("private-2.txt", b"Hello world 2\n")
343                .unwrap();
344        }
345        {
346            let mut f = checked
347                .file_access()
348                .create_with_mode(0o640)
349                .open(
350                    "public-1.txt",
351                    OpenOptions::new().write(true).create_new(true),
352                )
353                .unwrap();
354            f.write_all(b"Hello wider world\n").unwrap();
355
356            checked
357                .file_access()
358                .create_with_mode(0o644)
359                .write_and_replace("public-2.txt", b"Hello wider world 2")
360                .unwrap();
361        }
362
363        #[cfg(target_family = "unix")]
364        {
365            use std::os::unix::fs::MetadataExt;
366            assert_eq!(
367                fs::metadata(d.path("a/private-1.txt")).unwrap().mode() & 0o7777,
368                0o600
369            );
370            assert_eq!(
371                fs::metadata(d.path("a/private-2.txt")).unwrap().mode() & 0o7777,
372                0o600
373            );
374            assert_eq!(
375                fs::metadata(d.path("a/public-1.txt")).unwrap().mode() & 0o7777,
376                0o640
377            );
378            assert_eq!(
379                fs::metadata(d.path("a/public-2.txt")).unwrap().mode() & 0o7777,
380                0o644
381            );
382        }
383    }
384
385    #[test]
386    #[cfg(unix)]
387    fn open_symlinks() {
388        use crate::testing::LinkType;
389        let d = Dir::new();
390        d.dir("a");
391        d.dir("a/b");
392        d.dir("a/c");
393        d.file("a/c/file1.txt");
394        d.link_rel(LinkType::File, "../c/file1.txt", "a/b/present");
395        d.link_rel(LinkType::File, "../c/file2.txt", "a/b/absent");
396        d.chmod("a", 0o700);
397        d.chmod("a/b", 0o700);
398        d.chmod("a/c", 0o700);
399        d.chmod("a/c/file1.txt", 0o600);
400
401        let m = Mistrust::builder()
402            .ignore_prefix(d.canonical_root())
403            .build()
404            .unwrap();
405
406        // Try reading
407        let contents = m
408            .file_access()
409            .follow_final_links(true)
410            .read(d.path("a/b/present"))
411            .unwrap();
412        assert_eq!(
413            &contents[..],
414            &b"This space is intentionally left blank"[..]
415        );
416        let error = m
417            .file_access()
418            .follow_final_links(true)
419            .read(d.path("a/b/absent"))
420            .unwrap_err();
421        assert!(matches!(error, Error::NotFound(_)));
422
423        // Try writing.
424        {
425            let mut f = m
426                .file_access()
427                .follow_final_links(true)
428                .open(
429                    d.path("a/b/present"),
430                    OpenOptions::new().write(true).truncate(true),
431                )
432                .unwrap();
433            f.write_all(b"This is extremely serious!").unwrap();
434        }
435        let contents = m
436            .file_access()
437            .follow_final_links(true)
438            .read(d.path("a/b/present"))
439            .unwrap();
440        assert_eq!(&contents[..], &b"This is extremely serious!"[..]);
441
442        let contents = m.file_access().read(d.path("a/c/file1.txt")).unwrap();
443        assert_eq!(&contents[..], &b"This is extremely serious!"[..]);
444        {
445            let mut f = m
446                .file_access()
447                .follow_final_links(true)
448                .open(
449                    d.path("a/b/absent"),
450                    OpenOptions::new().create(true).write(true),
451                )
452                .unwrap();
453            f.write_all(b"This is extremely silly!").unwrap();
454        }
455        let contents = m.file_access().read(d.path("a/c/file2.txt")).unwrap();
456        assert_eq!(&contents[..], &b"This is extremely silly!"[..]);
457    }
458}