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 }