1use std::{
5 fs::{File, Metadata, OpenOptions},
6 io,
7 path::{Path, PathBuf},
8};
9
10use crate::{walk::PathType, Error, Mistrust, Result, Verifier};
11
12#[derive(Debug, Clone)]
32pub struct CheckedDir {
33 mistrust: Mistrust,
35 location: PathBuf,
37 readable_okay: bool,
39}
40
41impl CheckedDir {
42 pub(crate) fn new(verifier: &Verifier<'_>, path: &Path) -> Result<Self> {
44 let mut mistrust = verifier.mistrust.clone();
45 mistrust.ignore_prefix = crate::canonicalize_opt_prefix(&Some(Some(path.to_path_buf())))?;
53 Ok(CheckedDir {
54 mistrust,
55 location: path.to_path_buf(),
56 readable_okay: verifier.readable_okay,
57 })
58 }
59
60 pub fn make_directory<P: AsRef<Path>>(&self, path: P) -> Result<()> {
66 let path = path.as_ref();
67 self.check_path(path)?;
68 self.verifier().make_directory(self.location.join(path))
69 }
70
71 pub fn make_secure_directory<P: AsRef<Path>>(&self, path: P) -> Result<CheckedDir> {
78 let path = path.as_ref();
79 self.make_directory(path)?;
80 self.verifier().secure_dir(self.location.join(path))
82 }
83
84 pub fn file_access(&self) -> crate::FileAccess<'_> {
86 crate::FileAccess::from_checked_dir(self)
87 }
88
89 pub fn open<P: AsRef<Path>>(&self, path: P, options: &OpenOptions) -> Result<File> {
100 self.file_access().open(path, options)
101 }
102
103 pub fn read_directory<P: AsRef<Path>>(&self, path: P) -> Result<std::fs::ReadDir> {
112 let path = self.verified_full_path(path.as_ref(), FullPathCheck::CheckPath)?;
113
114 std::fs::read_dir(&path).map_err(|e| Error::io(e, path, "read directory"))
115 }
116
117 pub fn remove_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
125 let path = self.verified_full_path(path.as_ref(), FullPathCheck::CheckParent)?;
131
132 std::fs::remove_file(&path).map_err(|e| Error::io(e, path, "remove file"))
133 }
134
135 pub fn as_path(&self) -> &Path {
141 self.location.as_path()
142 }
143
144 pub fn join<P: AsRef<Path>>(&self, path: P) -> Result<PathBuf> {
150 let path = path.as_ref();
151 self.check_path(path)?;
152 Ok(self.location.join(path))
153 }
154
155 pub fn read_to_string<P: AsRef<Path>>(&self, path: P) -> Result<String> {
162 self.file_access().read_to_string(path)
163 }
164
165 pub fn read<P: AsRef<Path>>(&self, path: P) -> Result<Vec<u8>> {
172 self.file_access().read(path)
173 }
174
175 pub fn write_and_replace<P: AsRef<Path>, C: AsRef<[u8]>>(
192 &self,
193 path: P,
194 contents: C,
195 ) -> Result<()> {
196 self.file_access().write_and_replace(path, contents)
197 }
198
199 pub fn metadata<P: AsRef<Path>>(&self, path: P) -> Result<Metadata> {
214 let path = self.verified_full_path(path.as_ref(), FullPathCheck::CheckParent)?;
215
216 let meta = path
217 .symlink_metadata()
218 .map_err(|e| Error::inspecting(e, &path))?;
219
220 if meta.is_symlink() {
221 let err = io::Error::other(format!("Path {:?} is a symlink", path));
225 return Err(Error::io(err, &path, "metadata"));
226 }
227
228 if let Some(error) = self
229 .verifier()
230 .check_one(path.as_path(), PathType::Content, &meta)
231 .into_iter()
232 .next()
233 {
234 Err(error)
235 } else {
236 Ok(meta)
237 }
238 }
239
240 pub fn verifier(&self) -> Verifier<'_> {
243 let mut v = self.mistrust.verifier();
244 if self.readable_okay {
245 v = v.permit_readable();
246 }
247 v
248 }
249
250 fn check_path(&self, p: &Path) -> Result<()> {
255 use std::path::Component;
256 if p.is_absolute() {
258 return Err(Error::InvalidSubdirectory);
259 }
260
261 for component in p.components() {
262 match component {
263 Component::Prefix(_) | Component::RootDir | Component::ParentDir => {
264 return Err(Error::InvalidSubdirectory)
265 }
266 Component::CurDir | Component::Normal(_) => {}
267 }
268 }
269
270 Ok(())
271 }
272
273 pub(crate) fn verified_full_path(
277 &self,
278 p: &Path,
279 check_type: FullPathCheck,
280 ) -> Result<PathBuf> {
281 self.check_path(p)?;
282 let full_path = self.location.join(p);
283 let to_verify: &Path = match check_type {
284 FullPathCheck::CheckPath => full_path.as_ref(),
285 FullPathCheck::CheckParent => full_path.parent().unwrap_or_else(|| full_path.as_ref()),
286 };
287 self.verifier().check(to_verify)?;
288
289 Ok(full_path)
290 }
291}
292
293#[derive(Clone, Copy, Debug)]
295pub(crate) enum FullPathCheck {
296 CheckPath,
298 CheckParent,
300}
301
302#[cfg(test)]
303mod test {
304 #![allow(clippy::bool_assert_comparison)]
306 #![allow(clippy::clone_on_copy)]
307 #![allow(clippy::dbg_macro)]
308 #![allow(clippy::mixed_attributes_style)]
309 #![allow(clippy::print_stderr)]
310 #![allow(clippy::print_stdout)]
311 #![allow(clippy::single_char_pattern)]
312 #![allow(clippy::unwrap_used)]
313 #![allow(clippy::unchecked_duration_subtraction)]
314 #![allow(clippy::useless_vec)]
315 #![allow(clippy::needless_pass_by_value)]
316 use super::*;
318 use crate::testing::Dir;
319 use std::io::Write;
320
321 #[test]
322 fn easy_case() {
323 let d = Dir::new();
324 d.dir("a/b/c");
325 d.dir("a/b/d");
326 d.file("a/b/c/f1");
327 d.file("a/b/c/f2");
328 d.file("a/b/d/f3");
329
330 d.chmod("a", 0o755);
331 d.chmod("a/b", 0o700);
332 d.chmod("a/b/c", 0o700);
333 d.chmod("a/b/d", 0o777);
334 d.chmod("a/b/c/f1", 0o600);
335 d.chmod("a/b/c/f2", 0o666);
336 d.chmod("a/b/d/f3", 0o600);
337
338 let m = Mistrust::builder()
339 .ignore_prefix(d.canonical_root())
340 .build()
341 .unwrap();
342
343 let sd = m.verifier().secure_dir(d.path("a/b")).unwrap();
344
345 sd.make_directory("c/sub1").unwrap();
347 #[cfg(target_family = "unix")]
348 {
349 let e = sd.make_directory("d/sub2").unwrap_err();
350 assert!(matches!(e, Error::BadPermission(..)));
351 }
352
353 let f1 = sd.open("c/f1", OpenOptions::new().read(true)).unwrap();
355 drop(f1);
356 #[cfg(target_family = "unix")]
357 {
358 let e = sd.open("c/f2", OpenOptions::new().read(true)).unwrap_err();
359 assert!(matches!(e, Error::BadPermission(..)));
360 let e = sd.open("d/f3", OpenOptions::new().read(true)).unwrap_err();
361 assert!(matches!(e, Error::BadPermission(..)));
362 }
363
364 let mut f3 = sd
366 .open("c/f-new", OpenOptions::new().write(true).create(true))
367 .unwrap();
368 f3.write_all(b"Hello world").unwrap();
369 drop(f3);
370
371 #[cfg(target_family = "unix")]
372 {
373 let e = sd
374 .open("d/f-new", OpenOptions::new().write(true).create(true))
375 .unwrap_err();
376 assert!(matches!(e, Error::BadPermission(..)));
377 }
378 }
379
380 #[test]
381 fn bad_paths() {
382 let d = Dir::new();
383 d.dir("a");
384 d.chmod("a", 0o700);
385
386 let m = Mistrust::builder()
387 .ignore_prefix(d.canonical_root())
388 .build()
389 .unwrap();
390
391 let sd = m.verifier().secure_dir(d.path("a")).unwrap();
392
393 let e = sd.make_directory("hello/../world").unwrap_err();
394 assert!(matches!(e, Error::InvalidSubdirectory));
395 let e = sd.metadata("hello/../world").unwrap_err();
396 assert!(matches!(e, Error::InvalidSubdirectory));
397
398 let e = sd.make_directory("/hello").unwrap_err();
399 assert!(matches!(e, Error::InvalidSubdirectory));
400 let e = sd.metadata("/hello").unwrap_err();
401 assert!(matches!(e, Error::InvalidSubdirectory));
402
403 sd.make_directory("hello/world").unwrap();
404 }
405
406 #[test]
407 fn read_and_write() {
408 let d = Dir::new();
409 d.dir("a");
410 d.chmod("a", 0o700);
411 let m = Mistrust::builder()
412 .ignore_prefix(d.canonical_root())
413 .build()
414 .unwrap();
415
416 let checked = m.verifier().secure_dir(d.path("a")).unwrap();
417
418 checked
420 .write_and_replace("foo.txt", "this is incredibly silly")
421 .unwrap();
422
423 let s1 = checked.read_to_string("foo.txt").unwrap();
424 let s2 = checked.read("foo.txt").unwrap();
425 assert_eq!(s1, "this is incredibly silly");
426 assert_eq!(s1.as_bytes(), &s2[..]);
427
428 let sub = "sub";
430 let sub_checked = checked.make_secure_directory(sub).unwrap();
431 assert_eq!(sub_checked.as_path(), checked.as_path().join(sub));
432
433 checked
435 .open("bar.tmp", OpenOptions::new().create(true).write(true))
436 .unwrap()
437 .write_all("be the other guy".as_bytes())
438 .unwrap();
439 assert!(checked.join("bar.tmp").unwrap().try_exists().unwrap());
440
441 checked
442 .write_and_replace("bar.txt", "its hard and nobody understands")
443 .unwrap();
444
445 assert!(!checked.join("bar.tmp").unwrap().try_exists().unwrap());
447 let s4 = checked.read_to_string("bar.txt").unwrap();
448 assert_eq!(s4, "its hard and nobody understands");
449 }
450
451 #[test]
452 fn read_directory() {
453 let d = Dir::new();
454 d.dir("a");
455 d.chmod("a", 0o700);
456 d.dir("a/b");
457 d.file("a/b/f");
458 d.file("a/c.d");
459 d.dir("a/x");
460
461 d.chmod("a", 0o700);
462 d.chmod("a/b", 0o700);
463 d.chmod("a/x", 0o777);
464 let m = Mistrust::builder()
465 .ignore_prefix(d.canonical_root())
466 .build()
467 .unwrap();
468
469 let checked = m.verifier().secure_dir(d.path("a")).unwrap();
470
471 assert!(matches!(
472 checked.read_directory("/"),
473 Err(Error::InvalidSubdirectory)
474 ));
475 assert!(matches!(
476 checked.read_directory("b/.."),
477 Err(Error::InvalidSubdirectory)
478 ));
479 let mut members: Vec<String> = checked
480 .read_directory(".")
481 .unwrap()
482 .map(|ent| ent.unwrap().file_name().to_string_lossy().to_string())
483 .collect();
484 members.sort();
485 assert_eq!(members, vec!["b", "c.d", "x"]);
486
487 let members: Vec<String> = checked
488 .read_directory("b")
489 .unwrap()
490 .map(|ent| ent.unwrap().file_name().to_string_lossy().to_string())
491 .collect();
492 assert_eq!(members, vec!["f"]);
493
494 #[cfg(target_family = "unix")]
495 {
496 assert!(matches!(
497 checked.read_directory("x"),
498 Err(Error::BadPermission(_, _, _))
499 ));
500 }
501 }
502
503 #[test]
504 fn remove_file() {
505 let d = Dir::new();
506 d.dir("a");
507 d.chmod("a", 0o700);
508 d.dir("a/b");
509 d.file("a/b/f");
510 d.dir("a/b/d");
511 d.dir("a/x");
512 d.dir("a/x/y");
513 d.file("a/x/y/z");
514
515 d.chmod("a", 0o700);
516 d.chmod("a/b", 0o700);
517 d.chmod("a/x", 0o777);
518
519 let m = Mistrust::builder()
520 .ignore_prefix(d.canonical_root())
521 .build()
522 .unwrap();
523 let checked = m.verifier().secure_dir(d.path("a")).unwrap();
524
525 assert!(checked.read_to_string("b/f").is_ok());
527 assert!(checked.metadata("b/f").unwrap().is_file());
528 checked.remove_file("b/f").unwrap();
529 assert!(matches!(
530 checked.read_to_string("b/f"),
531 Err(Error::NotFound(_))
532 ));
533 assert!(matches!(checked.metadata("b/f"), Err(Error::NotFound(_))));
534 assert!(matches!(
535 checked.remove_file("b/f"),
536 Err(Error::NotFound(_))
537 ));
538
539 assert!(matches!(
541 checked.remove_file("b/xyzzy/fred"),
542 Err(Error::NotFound(_))
543 ));
544
545 #[cfg(target_family = "unix")]
547 {
548 assert!(matches!(
549 checked.remove_file("x/y/z"),
550 Err(Error::BadPermission(_, _, _))
551 ));
552 assert!(matches!(
553 checked.metadata("x/y/z"),
554 Err(Error::BadPermission(_, _, _))
555 ));
556 }
557 }
558
559 #[test]
560 #[cfg(target_family = "unix")]
561 fn access_symlink() {
562 use crate::testing::LinkType;
563
564 let d = Dir::new();
565 d.dir("a/b");
566 d.file("a/b/f1");
567
568 d.chmod("a/b", 0o700);
569 d.chmod("a/b/f1", 0o600);
570 d.link_rel(LinkType::File, "f1", "a/b/f1-link");
571
572 let m = Mistrust::builder()
573 .ignore_prefix(d.canonical_root())
574 .build()
575 .unwrap();
576
577 let sd = m.verifier().secure_dir(d.path("a/b")).unwrap();
578
579 assert!(sd.open("f1", OpenOptions::new().read(true)).is_ok());
580
581 let e = sd.metadata("f1-link").unwrap_err();
583 assert!(
584 matches!(e, Error::Io { ref err, .. } if err.to_string().contains("is a symlink")),
585 "{e:?}"
586 );
587
588 let e = sd
590 .open("f1-link", OpenOptions::new().read(true))
591 .unwrap_err();
592 assert!(
593 matches!(e, Error::Io { ref err, .. } if err.to_string().contains("symbolic")), "{e:?}"
595 );
596 }
597}