/ crates / fs-mistrust / src / dir.rs
dir.rs
  1  //! Implement a wrapper for access to the members of a directory whose status
  2  //! we've checked.
  3  
  4  use std::{
  5      fs::{File, Metadata, OpenOptions},
  6      io::{self, Read, Write},
  7      path::{Path, PathBuf},
  8  };
  9  
 10  use crate::{walk::PathType, Error, Mistrust, Result, Verifier};
 11  
 12  #[cfg(target_family = "unix")]
 13  use std::os::unix::fs::OpenOptionsExt;
 14  
 15  /// A directory whose access properties we have verified, along with accessor
 16  /// functions to access members of that directory.
 17  ///
 18  /// The accessor functions will enforce that whatever security properties we
 19  /// checked on the directory also apply to all of the members that we access
 20  /// within the directory.
 21  ///
 22  /// ## Limitations
 23  ///
 24  /// Having a `CheckedDir` means only that, at the time it was created, we were
 25  /// confident that no _untrusted_ user could access it inappropriately.  It is
 26  /// still possible, after the `CheckedDir` is created, that a _trusted_ user can
 27  /// alter its permissions, make its path point somewhere else, or so forth.
 28  ///
 29  /// If this kind of time-of-use/time-of-check issue is unacceptable, you may
 30  /// wish to look at other solutions, possibly involving `openat()` or related
 31  /// APIs.
 32  ///
 33  /// See also the crate-level [Limitations](crate#limitations) section.
 34  #[derive(Debug, Clone)]
 35  pub struct CheckedDir {
 36      /// The `Mistrust` object whose rules we apply to members of this directory.
 37      mistrust: Mistrust,
 38      /// The location of this directory, in its original form.
 39      location: PathBuf,
 40      /// The "readable_okay" flag that we used to create this CheckedDir.
 41      readable_okay: bool,
 42  }
 43  
 44  impl CheckedDir {
 45      /// Create a CheckedDir.
 46      pub(crate) fn new(verifier: &Verifier<'_>, path: &Path) -> Result<Self> {
 47          let mut mistrust = verifier.mistrust.clone();
 48          // Ignore the path that we already verified.  Since ignore_prefix
 49          // canonicalizes the path, we _will_ recheck the directory if it starts
 50          // pointing to a new canonical location.  That's probably a feature.
 51          //
 52          // TODO:
 53          //   * If `path` is a prefix of the original ignored path, this will
 54          //     make us ignore _less_.
 55          mistrust.ignore_prefix = crate::canonicalize_opt_prefix(&Some(Some(path.to_path_buf())))?;
 56          Ok(CheckedDir {
 57              mistrust,
 58              location: path.to_path_buf(),
 59              readable_okay: verifier.readable_okay,
 60          })
 61      }
 62  
 63      /// Construct a new directory within this CheckedDir, if it does not already
 64      /// exist.
 65      ///
 66      /// `path` must be a relative path to the new directory, containing no `..`
 67      /// components.
 68      pub fn make_directory<P: AsRef<Path>>(&self, path: P) -> Result<()> {
 69          let path = path.as_ref();
 70          self.check_path(path)?;
 71          self.verifier().make_directory(self.location.join(path))
 72      }
 73  
 74      /// Construct a new `CheckedDir` within this `CheckedDir`
 75      ///
 76      /// Creates the directory if it does not already exist.
 77      ///
 78      /// `path` must be a relative path to the new directory, containing no `..`
 79      /// components.
 80      pub fn make_secure_directory<P: AsRef<Path>>(&self, path: P) -> Result<CheckedDir> {
 81          let path = path.as_ref();
 82          self.make_directory(path)?;
 83          // TODO I think this rechecks parents, but it need not, since we already did that.
 84          self.verifier().secure_dir(self.location.join(path))
 85      }
 86  
 87      /// Open a file within this CheckedDir, using a set of [`OpenOptions`].
 88      ///
 89      /// `path` must be a relative path to the new directory, containing no `..`
 90      /// components.  We check, but do not create, the file's parent directories.
 91      /// We check the file's permissions after opening it.  If the file already
 92      /// exists, it must not be a symlink.
 93      ///
 94      /// If the file is created (and this is a unix-like operating system), we
 95      /// always create it with mode `600`, regardless of any mode options set in
 96      /// `options`.
 97      pub fn open<P: AsRef<Path>>(&self, path: P, options: &OpenOptions) -> Result<File> {
 98          let path = path.as_ref();
 99          self.check_path(path)?;
100          let path = self.location.join(path);
101          if let Some(parent) = path.parent() {
102              self.verifier().check(parent)?;
103          }
104  
105          #[allow(unused_mut)]
106          let mut options = options.clone();
107  
108          #[cfg(target_family = "unix")]
109          {
110              // By default, create all files mode 600, no matter what
111              // OpenOptions said.
112  
113              // TODO: Give some way to override this to 640 or 0644 if you
114              //    really want to.
115              options.mode(0o600);
116              // Don't follow symlinks out of the secured directory.
117              options.custom_flags(libc::O_NOFOLLOW);
118          }
119  
120          let file = options
121              .open(&path)
122              .map_err(|e| Error::io(e, &path, "open file"))?;
123          let meta = file.metadata().map_err(|e| Error::inspecting(e, &path))?;
124  
125          if let Some(error) = self
126              .verifier()
127              .check_one(path.as_path(), PathType::Content, &meta)
128              .into_iter()
129              .next()
130          {
131              Err(error)
132          } else {
133              Ok(file)
134          }
135      }
136  
137      /// List the contents of a directory within this [`CheckedDir`].
138      ///
139      /// `path` must be a relative path, containing no `..` components.  Before
140      /// listing the directory, we verify that that no untrusted user is able
141      /// change its contents or make it point somewhere else.
142      ///
143      /// The return value is an iterator as returned by [`std::fs::ReadDir`].  We
144      /// _do not_ check any properties of the elements of this iterator.
145      pub fn read_directory<P: AsRef<Path>>(&self, path: P) -> Result<std::fs::ReadDir> {
146          let path = path.as_ref();
147          self.check_path(path)?;
148          let path = self.location.join(path);
149          self.verifier().check(&path)?;
150  
151          std::fs::read_dir(&path).map_err(|e| Error::io(e, path, "read directory"))
152      }
153  
154      /// Remove a file within this [`CheckedDir`].
155      ///
156      /// `path` must be a relative path, containing no `..` components.
157      ///
158      /// Note that we ensure that the _parent_ of the file to be removed is
159      /// unmodifiable by any untrusted user, but we do not check any permissions
160      /// on the file itself, since those are irrelevant to removing it.
161      pub fn remove_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
162          let path = path.as_ref();
163          self.check_path(path)?;
164          let path = self.location.join(path);
165          // We insist that the ownership and permissions on everything up to and
166          // including the _parent_ of the path that we are removing have to be
167          // correct.  (If it were otherwise, we could be tricked into removing
168          // the wrong thing.)  But we don't care about the permissions on file we
169          // are removing.
170          if let Some(parent) = path.parent() {
171              self.verifier().check(parent)?;
172          }
173  
174          std::fs::remove_file(&path).map_err(|e| Error::io(e, path, "remove file"))
175      }
176  
177      /// Return a reference to this directory as a [`Path`].
178      ///
179      /// Note that this function lets you work with a broader collection of
180      /// functions, including functions that might let you access or create a
181      /// file that is accessible by non-trusted users.  Be careful!
182      pub fn as_path(&self) -> &Path {
183          self.location.as_path()
184      }
185  
186      /// Return a new [`PathBuf`] containing this directory's path, with `path`
187      /// appended to it.
188      ///
189      /// Return an error if `path` has any components that could take us outside
190      /// of this directory.
191      pub fn join<P: AsRef<Path>>(&self, path: P) -> Result<PathBuf> {
192          let path = path.as_ref();
193          self.check_path(path)?;
194          Ok(self.location.join(path))
195      }
196  
197      /// Read the contents of the file at `path` within this directory, as a
198      /// String, if possible.
199      ///
200      /// Return an error if `path` is absent, if its permissions are incorrect,
201      /// if it has any components that could take us outside of this directory,
202      /// or if its contents are not UTF-8.
203      pub fn read_to_string<P: AsRef<Path>>(&self, path: P) -> Result<String> {
204          let path = path.as_ref();
205          let mut file = self.open(path, OpenOptions::new().read(true))?;
206          let mut result = String::new();
207          file.read_to_string(&mut result)
208              .map_err(|e| Error::io(e, path, "read file"))?;
209          Ok(result)
210      }
211  
212      /// Read the contents of the file at `path` within this directory, as a
213      /// vector of bytes, if possible.
214      ///
215      /// Return an error if `path` is absent, if its permissions are incorrect,
216      /// or if it has any components that could take us outside of this
217      /// directory.
218      pub fn read<P: AsRef<Path>>(&self, path: P) -> Result<Vec<u8>> {
219          let path = path.as_ref();
220          let mut file = self.open(path, OpenOptions::new().read(true))?;
221          let mut result = Vec::new();
222          file.read_to_end(&mut result)
223              .map_err(|e| Error::io(e, path, "read file"))?;
224          Ok(result)
225      }
226  
227      /// Store `contents` into the file located at `path` within this directory.
228      ///
229      /// We won't write to `path` directly: instead, we'll write to a temporary
230      /// file in the same directory as `path`, and then replace `path` with that
231      /// temporary file if we were successful.  (This isn't truly atomic on all
232      /// file systems, but it's closer than many alternatives.)
233      ///
234      /// # Limitations
235      ///
236      /// This function will clobber any existing files with the same name as
237      /// `path` but with the extension `tmp`.  (That is, if you are writing to
238      /// "foo.txt", it will replace "foo.tmp" in the same directory.)
239      ///
240      /// This function may give incorrect behavior if multiple threads or
241      /// processes are writing to the same file at the same time: it is the
242      /// programmer's responsibility to use appropriate locking to avoid this.
243      pub fn write_and_replace<P: AsRef<Path>, C: AsRef<[u8]>>(
244          &self,
245          path: P,
246          contents: C,
247      ) -> Result<()> {
248          let path = path.as_ref();
249          self.check_path(path)?;
250  
251          let tmp_name = path.with_extension("tmp");
252          let mut tmp_file = self.open(
253              &tmp_name,
254              OpenOptions::new().create(true).truncate(true).write(true),
255          )?;
256  
257          // Write the data.
258          tmp_file
259              .write_all(contents.as_ref())
260              .map_err(|e| Error::io(e, &tmp_name, "write to file"))?;
261          // Flush and close.
262          drop(tmp_file);
263  
264          // Replace the old file.
265          std::fs::rename(self.location.join(tmp_name), self.location.join(path))
266              .map_err(|e| Error::io(e, path, "replace file"))?;
267          Ok(())
268      }
269  
270      /// Return the [`Metadata`] of the file located at `path`.
271      ///
272      /// `path` must be a relative path, containing no `..` components.
273      /// We check the file's parent directories,
274      /// and the file's permissions.
275      /// If the file exists, it must not be a symlink.
276      ///
277      /// Returns [`Error::NotFound`] if the file does not exist.
278      ///
279      /// Return an error if `path` is absent, if its permissions are incorrect[^1],
280      /// if the permissions of any of its the parent directories are incorrect,
281      /// or if it has any components that could take us outside of this directory.
282      ///
283      /// [^1]: the permissions are incorrect if the path is readable or writable by untrusted users
284      pub fn metadata<P: AsRef<Path>>(&self, path: P) -> Result<Metadata> {
285          let path = path.as_ref();
286          self.check_path(path)?;
287          let path = self.location.join(path);
288          if let Some(parent) = path.parent() {
289              self.verifier().check(parent)?;
290          }
291  
292          let meta = path
293              .symlink_metadata()
294              .map_err(|e| Error::inspecting(e, &path))?;
295  
296          if meta.is_symlink() {
297              // TODO: this is inconsistent with CheckedDir::open()'s behavior, which returns a
298              // FilesystemLoop io error in this case (we can't construct such an error here, because
299              // ErrorKind::FilesystemLoop is only available on nightly)
300              let err = io::Error::new(
301                  io::ErrorKind::Other,
302                  format!("Path {:?} is a symlink", path),
303              );
304              return Err(Error::io(err, &path, "metadata"));
305          }
306  
307          if let Some(error) = self
308              .verifier()
309              .check_one(path.as_path(), PathType::Content, &meta)
310              .into_iter()
311              .next()
312          {
313              Err(error)
314          } else {
315              Ok(meta)
316          }
317      }
318  
319      /// Create a [`Verifier`] with the appropriate rules for this
320      /// `CheckedDir`.
321      pub fn verifier(&self) -> Verifier<'_> {
322          let mut v = self.mistrust.verifier();
323          if self.readable_okay {
324              v = v.permit_readable();
325          }
326          v
327      }
328  
329      /// Helper: Make sure that the path `p` is a relative path that can be
330      /// guaranteed to stay within this directory.
331      fn check_path(&self, p: &Path) -> Result<()> {
332          use std::path::Component;
333          // This check should be redundant, but let's be certain.
334          if p.is_absolute() {
335              return Err(Error::InvalidSubdirectory);
336          }
337  
338          for component in p.components() {
339              match component {
340                  Component::Prefix(_) | Component::RootDir | Component::ParentDir => {
341                      return Err(Error::InvalidSubdirectory)
342                  }
343                  Component::CurDir | Component::Normal(_) => {}
344              }
345          }
346  
347          Ok(())
348      }
349  }
350  
351  #[cfg(test)]
352  mod test {
353      // @@ begin test lint list maintained by maint/add_warning @@
354      #![allow(clippy::bool_assert_comparison)]
355      #![allow(clippy::clone_on_copy)]
356      #![allow(clippy::dbg_macro)]
357      #![allow(clippy::mixed_attributes_style)]
358      #![allow(clippy::print_stderr)]
359      #![allow(clippy::print_stdout)]
360      #![allow(clippy::single_char_pattern)]
361      #![allow(clippy::unwrap_used)]
362      #![allow(clippy::unchecked_duration_subtraction)]
363      #![allow(clippy::useless_vec)]
364      #![allow(clippy::needless_pass_by_value)]
365      //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
366      use super::*;
367      use crate::testing::Dir;
368      use std::io::Write;
369  
370      #[test]
371      fn easy_case() {
372          let d = Dir::new();
373          d.dir("a/b/c");
374          d.dir("a/b/d");
375          d.file("a/b/c/f1");
376          d.file("a/b/c/f2");
377          d.file("a/b/d/f3");
378  
379          d.chmod("a", 0o755);
380          d.chmod("a/b", 0o700);
381          d.chmod("a/b/c", 0o700);
382          d.chmod("a/b/d", 0o777);
383          d.chmod("a/b/c/f1", 0o600);
384          d.chmod("a/b/c/f2", 0o666);
385          d.chmod("a/b/d/f3", 0o600);
386  
387          let m = Mistrust::builder()
388              .ignore_prefix(d.canonical_root())
389              .build()
390              .unwrap();
391  
392          let sd = m.verifier().secure_dir(d.path("a/b")).unwrap();
393  
394          // Try make_directory.
395          sd.make_directory("c/sub1").unwrap();
396          #[cfg(target_family = "unix")]
397          {
398              let e = sd.make_directory("d/sub2").unwrap_err();
399              assert!(matches!(e, Error::BadPermission(..)));
400          }
401  
402          // Try opening a file that exists.
403          let f1 = sd.open("c/f1", OpenOptions::new().read(true)).unwrap();
404          drop(f1);
405          #[cfg(target_family = "unix")]
406          {
407              let e = sd.open("c/f2", OpenOptions::new().read(true)).unwrap_err();
408              assert!(matches!(e, Error::BadPermission(..)));
409              let e = sd.open("d/f3", OpenOptions::new().read(true)).unwrap_err();
410              assert!(matches!(e, Error::BadPermission(..)));
411          }
412  
413          // Try creating a file.
414          let mut f3 = sd
415              .open("c/f-new", OpenOptions::new().write(true).create(true))
416              .unwrap();
417          f3.write_all(b"Hello world").unwrap();
418          drop(f3);
419  
420          #[cfg(target_family = "unix")]
421          {
422              let e = sd
423                  .open("d/f-new", OpenOptions::new().write(true).create(true))
424                  .unwrap_err();
425              assert!(matches!(e, Error::BadPermission(..)));
426          }
427      }
428  
429      #[test]
430      fn bad_paths() {
431          let d = Dir::new();
432          d.dir("a");
433          d.chmod("a", 0o700);
434  
435          let m = Mistrust::builder()
436              .ignore_prefix(d.canonical_root())
437              .build()
438              .unwrap();
439  
440          let sd = m.verifier().secure_dir(d.path("a")).unwrap();
441  
442          let e = sd.make_directory("hello/../world").unwrap_err();
443          assert!(matches!(e, Error::InvalidSubdirectory));
444          let e = sd.metadata("hello/../world").unwrap_err();
445          assert!(matches!(e, Error::InvalidSubdirectory));
446  
447          let e = sd.make_directory("/hello").unwrap_err();
448          assert!(matches!(e, Error::InvalidSubdirectory));
449          let e = sd.metadata("/hello").unwrap_err();
450          assert!(matches!(e, Error::InvalidSubdirectory));
451  
452          sd.make_directory("hello/world").unwrap();
453      }
454  
455      #[test]
456      fn read_and_write() {
457          let d = Dir::new();
458          d.dir("a");
459          d.chmod("a", 0o700);
460          let m = Mistrust::builder()
461              .ignore_prefix(d.canonical_root())
462              .build()
463              .unwrap();
464  
465          let checked = m.verifier().secure_dir(d.path("a")).unwrap();
466  
467          // Simple case: write and read.
468          checked
469              .write_and_replace("foo.txt", "this is incredibly silly")
470              .unwrap();
471  
472          let s1 = checked.read_to_string("foo.txt").unwrap();
473          let s2 = checked.read("foo.txt").unwrap();
474          assert_eq!(s1, "this is incredibly silly");
475          assert_eq!(s1.as_bytes(), &s2[..]);
476  
477          // Checked subdirectory
478          let sub = "sub";
479          let sub_checked = checked.make_secure_directory(sub).unwrap();
480          assert_eq!(sub_checked.as_path(), checked.as_path().join(sub));
481  
482          // Trickier: write when the preferred temporary already has content.
483          checked
484              .open("bar.tmp", OpenOptions::new().create(true).write(true))
485              .unwrap()
486              .write_all("be the other guy".as_bytes())
487              .unwrap();
488          assert!(checked.join("bar.tmp").unwrap().try_exists().unwrap());
489  
490          checked
491              .write_and_replace("bar.txt", "its hard and nobody understands")
492              .unwrap();
493  
494          // Temp file should be gone.
495          assert!(!checked.join("bar.tmp").unwrap().try_exists().unwrap());
496          let s4 = checked.read_to_string("bar.txt").unwrap();
497          assert_eq!(s4, "its hard and nobody understands");
498      }
499  
500      #[test]
501      fn read_directory() {
502          let d = Dir::new();
503          d.dir("a");
504          d.chmod("a", 0o700);
505          d.dir("a/b");
506          d.file("a/b/f");
507          d.file("a/c.d");
508          d.dir("a/x");
509  
510          d.chmod("a", 0o700);
511          d.chmod("a/b", 0o700);
512          d.chmod("a/x", 0o777);
513          let m = Mistrust::builder()
514              .ignore_prefix(d.canonical_root())
515              .build()
516              .unwrap();
517  
518          let checked = m.verifier().secure_dir(d.path("a")).unwrap();
519  
520          assert!(matches!(
521              checked.read_directory("/"),
522              Err(Error::InvalidSubdirectory)
523          ));
524          assert!(matches!(
525              checked.read_directory("b/.."),
526              Err(Error::InvalidSubdirectory)
527          ));
528          let mut members: Vec<String> = checked
529              .read_directory(".")
530              .unwrap()
531              .map(|ent| ent.unwrap().file_name().to_string_lossy().to_string())
532              .collect();
533          members.sort();
534          assert_eq!(members, vec!["b", "c.d", "x"]);
535  
536          let members: Vec<String> = checked
537              .read_directory("b")
538              .unwrap()
539              .map(|ent| ent.unwrap().file_name().to_string_lossy().to_string())
540              .collect();
541          assert_eq!(members, vec!["f"]);
542  
543          #[cfg(target_family = "unix")]
544          {
545              assert!(matches!(
546                  checked.read_directory("x"),
547                  Err(Error::BadPermission(_, _, _))
548              ));
549          }
550      }
551  
552      #[test]
553      fn remove_file() {
554          let d = Dir::new();
555          d.dir("a");
556          d.chmod("a", 0o700);
557          d.dir("a/b");
558          d.file("a/b/f");
559          d.dir("a/b/d");
560          d.dir("a/x");
561          d.dir("a/x/y");
562          d.file("a/x/y/z");
563  
564          d.chmod("a", 0o700);
565          d.chmod("a/b", 0o700);
566          d.chmod("a/x", 0o777);
567  
568          let m = Mistrust::builder()
569              .ignore_prefix(d.canonical_root())
570              .build()
571              .unwrap();
572          let checked = m.verifier().secure_dir(d.path("a")).unwrap();
573  
574          // Remove a file that is there, and then make sure it is gone.
575          assert!(checked.read_to_string("b/f").is_ok());
576          assert!(checked.metadata("b/f").unwrap().is_file());
577          checked.remove_file("b/f").unwrap();
578          assert!(matches!(
579              checked.read_to_string("b/f"),
580              Err(Error::NotFound(_))
581          ));
582          assert!(matches!(checked.metadata("b/f"), Err(Error::NotFound(_))));
583          assert!(matches!(
584              checked.remove_file("b/f"),
585              Err(Error::NotFound(_))
586          ));
587  
588          // Remove a file in a nonexistent subdirectory
589          assert!(matches!(
590              checked.remove_file("b/xyzzy/fred"),
591              Err(Error::NotFound(_))
592          ));
593  
594          // Remove a file in a directory whose permissions are too open.
595          #[cfg(target_family = "unix")]
596          {
597              assert!(matches!(
598                  checked.remove_file("x/y/z"),
599                  Err(Error::BadPermission(_, _, _))
600              ));
601              assert!(matches!(
602                  checked.metadata("x/y/z"),
603                  Err(Error::BadPermission(_, _, _))
604              ));
605          }
606      }
607  
608      #[test]
609      #[cfg(target_family = "unix")]
610      fn metadata_symlink() {
611          use crate::testing::LinkType;
612  
613          let d = Dir::new();
614          d.dir("a/b");
615          d.file("a/b/f1");
616  
617          d.chmod("a/b", 0o700);
618          d.chmod("a/b/f1", 0o600);
619          d.link_rel(LinkType::File, "f1", "a/b/f1-link");
620  
621          let m = Mistrust::builder()
622              .ignore_prefix(d.canonical_root())
623              .build()
624              .unwrap();
625  
626          let sd = m.verifier().secure_dir(d.path("a/b")).unwrap();
627  
628          assert!(sd.open("f1", OpenOptions::new().read(true)).is_ok());
629  
630          // Metadata returns an error if called on a symlink
631          let e = sd.metadata("f1-link").unwrap_err();
632          assert!(
633              matches!(e, Error::Io { ref err, .. } if err.to_string().contains("is a symlink")),
634              "{e:?}"
635          );
636      }
637  }