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,
7
    path::{Path, PathBuf},
8
};
9

            
10
use crate::{Error, Mistrust, Result, Verifier, walk::PathType};
11

            
12
/// A directory whose access properties we have verified, along with accessor
13
/// functions to access members of that directory.
14
///
15
/// The accessor functions will enforce that whatever security properties we
16
/// checked on the directory also apply to all of the members that we access
17
/// within the directory.
18
///
19
/// ## Limitations
20
///
21
/// Having a `CheckedDir` means only that, at the time it was created, we were
22
/// confident that no _untrusted_ user could access it inappropriately.  It is
23
/// still possible, after the `CheckedDir` is created, that a _trusted_ user can
24
/// alter its permissions, make its path point somewhere else, or so forth.
25
///
26
/// If this kind of time-of-use/time-of-check issue is unacceptable, you may
27
/// wish to look at other solutions, possibly involving `openat()` or related
28
/// APIs.
29
///
30
/// See also the crate-level [Limitations](crate#limitations) section.
31
#[derive(Debug, Clone)]
32
pub struct CheckedDir {
33
    /// The `Mistrust` object whose rules we apply to members of this directory.
34
    mistrust: Mistrust,
35
    /// The location of this directory, in its original form.
36
    location: PathBuf,
37
    /// The "readable_okay" flag that we used to create this CheckedDir.
38
    readable_okay: bool,
39
}
40

            
41
impl CheckedDir {
42
    /// Create a CheckedDir.
43
31552
    pub(crate) fn new(verifier: &Verifier<'_>, path: &Path) -> Result<Self> {
44
31552
        let mut mistrust = verifier.mistrust.clone();
45
        // Ignore the path that we already verified.  Since ignore_prefix
46
        // canonicalizes the path, we _will_ recheck the directory if it starts
47
        // pointing to a new canonical location.  That's probably a feature.
48
        //
49
        // TODO:
50
        //   * If `path` is a prefix of the original ignored path, this will
51
        //     make us ignore _less_.
52
31552
        mistrust.ignore_prefix = crate::canonicalize_opt_prefix(&Some(Some(path.to_path_buf())))?;
53
31552
        Ok(CheckedDir {
54
31552
            mistrust,
55
31552
            location: path.to_path_buf(),
56
31552
            readable_okay: verifier.readable_okay,
57
31552
        })
58
31552
    }
59

            
60
    /// Construct a new directory within this CheckedDir, if it does not already
61
    /// exist.
62
    ///
63
    /// `path` must be a relative path to the new directory, containing no `..`
64
    /// components.
65
6026
    pub fn make_directory<P: AsRef<Path>>(&self, path: P) -> Result<()> {
66
6026
        let path = path.as_ref();
67
6026
        self.check_path(path)?;
68
6022
        self.verifier().make_directory(self.location.join(path))
69
6026
    }
70

            
71
    /// Construct a new `CheckedDir` within this `CheckedDir`
72
    ///
73
    /// Creates the directory if it does not already exist.
74
    ///
75
    /// `path` must be a relative path to the new directory, containing no `..`
76
    /// components.
77
2726
    pub fn make_secure_directory<P: AsRef<Path>>(&self, path: P) -> Result<CheckedDir> {
78
2726
        let path = path.as_ref();
79
2726
        self.make_directory(path)?;
80
        // TODO I think this rechecks parents, but it need not, since we already did that.
81
2726
        self.verifier().secure_dir(self.location.join(path))
82
2726
    }
83

            
84
    /// Create a new [`FileAccess`](crate::FileAccess) for reading or writing files within this directory.
85
72819
    pub fn file_access(&self) -> crate::FileAccess<'_> {
86
72819
        crate::FileAccess::from_checked_dir(self)
87
72819
    }
88

            
89
    /// Open a file within this CheckedDir, using a set of [`OpenOptions`].
90
    ///
91
    /// `path` must be a relative path to the new directory, containing no `..`
92
    /// components.  We check, but do not create, the file's parent directories.
93
    /// We check the file's permissions after opening it.  If the file already
94
    /// exists, it must not be a symlink.
95
    ///
96
    /// If the file is created (and this is a unix-like operating system), we
97
    /// always create it with mode `600`, regardless of any mode options set in
98
    /// `options`.
99
38
    pub fn open<P: AsRef<Path>>(&self, path: P, options: &OpenOptions) -> Result<File> {
100
38
        self.file_access().open(path, options)
101
38
    }
102

            
103
    /// List the contents of a directory within this [`CheckedDir`].
104
    ///
105
    /// `path` must be a relative path, containing no `..` components.  Before
106
    /// listing the directory, we verify that that no untrusted user is able
107
    /// change its contents or make it point somewhere else.
108
    ///
109
    /// The return value is an iterator as returned by [`std::fs::ReadDir`].  We
110
    /// _do not_ check any properties of the elements of this iterator.
111
7492
    pub fn read_directory<P: AsRef<Path>>(&self, path: P) -> Result<std::fs::ReadDir> {
112
7492
        let path = self.verified_full_path(path.as_ref(), FullPathCheck::CheckPath)?;
113

            
114
7484
        std::fs::read_dir(&path).map_err(|e| Error::io(e, path, "read directory"))
115
7492
    }
116

            
117
    /// Remove a file within this [`CheckedDir`].
118
    ///
119
    /// `path` must be a relative path, containing no `..` components.
120
    ///
121
    /// Note that we ensure that the _parent_ of the file to be removed is
122
    /// unmodifiable by any untrusted user, but we do not check any permissions
123
    /// on the file itself, since those are irrelevant to removing it.
124
1528
    pub fn remove_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
125
        // We insist that the ownership and permissions on everything up to and
126
        // including the _parent_ of the path that we are removing have to be
127
        // correct.  (If it were otherwise, we could be tricked into removing
128
        // the wrong thing.)  But we don't care about the permissions on file we
129
        // are removing.
130
1528
        let path = self.verified_full_path(path.as_ref(), FullPathCheck::CheckParent)?;
131

            
132
1477
        std::fs::remove_file(&path).map_err(|e| Error::io(e, path, "remove file"))
133
1528
    }
134

            
135
    /// Return a reference to this directory as a [`Path`].
136
    ///
137
    /// Note that this function lets you work with a broader collection of
138
    /// functions, including functions that might let you access or create a
139
    /// file that is accessible by non-trusted users.  Be careful!
140
56433
    pub fn as_path(&self) -> &Path {
141
56433
        self.location.as_path()
142
56433
    }
143

            
144
    /// Return a new [`PathBuf`] containing this directory's path, with `path`
145
    /// appended to it.
146
    ///
147
    /// Return an error if `path` has any components that could take us outside
148
    /// of this directory.
149
42010
    pub fn join<P: AsRef<Path>>(&self, path: P) -> Result<PathBuf> {
150
42010
        let path = path.as_ref();
151
42010
        self.check_path(path)?;
152
41869
        Ok(self.location.join(path))
153
42010
    }
154

            
155
    /// Read the contents of the file at `path` within this directory, as a
156
    /// String, if possible.
157
    ///
158
    /// Return an error if `path` is absent, if its permissions are incorrect,
159
    /// if it has any components that could take us outside of this directory,
160
    /// or if its contents are not UTF-8.
161
2302
    pub fn read_to_string<P: AsRef<Path>>(&self, path: P) -> Result<String> {
162
2302
        self.file_access().read_to_string(path)
163
2302
    }
164

            
165
    /// Read the contents of the file at `path` within this directory, as a
166
    /// vector of bytes, if possible.
167
    ///
168
    /// Return an error if `path` is absent, if its permissions are incorrect,
169
    /// or if it has any components that could take us outside of this
170
    /// directory.
171
23310
    pub fn read<P: AsRef<Path>>(&self, path: P) -> Result<Vec<u8>> {
172
23310
        self.file_access().read(path)
173
23310
    }
174

            
175
    /// Store `contents` into the file located at `path` within this directory.
176
    ///
177
    /// We won't write to `path` directly: instead, we'll write to a temporary
178
    /// file in the same directory as `path`, and then replace `path` with that
179
    /// temporary file if we were successful.  (This isn't truly atomic on all
180
    /// file systems, but it's closer than many alternatives.)
181
    ///
182
    /// # Limitations
183
    ///
184
    /// This function will clobber any existing files with the same name as
185
    /// `path` but with the extension `tmp`.  (That is, if you are writing to
186
    /// "foo.txt", it will replace "foo.tmp" in the same directory.)
187
    ///
188
    /// This function may give incorrect behavior if multiple threads or
189
    /// processes are writing to the same file at the same time: it is the
190
    /// programmer's responsibility to use appropriate locking to avoid this.
191
2996
    pub fn write_and_replace<P: AsRef<Path>, C: AsRef<[u8]>>(
192
2996
        &self,
193
2996
        path: P,
194
2996
        contents: C,
195
2996
    ) -> Result<()> {
196
2996
        self.file_access().write_and_replace(path, contents)
197
2996
    }
198

            
199
    /// Return the [`Metadata`] of the file located at `path`.
200
    ///
201
    /// `path` must be a relative path, containing no `..` components.
202
    /// We check the file's parent directories,
203
    /// and the file's permissions.
204
    /// If the file exists, it must not be a symlink.
205
    ///
206
    /// Returns [`Error::NotFound`] if the file does not exist.
207
    ///
208
    /// Return an error if `path` is absent, if its permissions are incorrect[^1],
209
    /// if the permissions of any of its the parent directories are incorrect,
210
    /// or if it has any components that could take us outside of this directory.
211
    ///
212
    /// [^1]: the permissions are incorrect if the path is readable or writable by untrusted users
213
1310
    pub fn metadata<P: AsRef<Path>>(&self, path: P) -> Result<Metadata> {
214
1310
        let path = self.verified_full_path(path.as_ref(), FullPathCheck::CheckParent)?;
215

            
216
1064
        let meta = path
217
1064
            .symlink_metadata()
218
1064
            .map_err(|e| Error::inspecting(e, &path))?;
219

            
220
22
        if meta.is_symlink() {
221
            // TODO: this is inconsistent with CheckedDir::open()'s behavior, which returns a
222
            // FilesystemLoop io error in this case (we can't construct such an error here, because
223
            // ErrorKind::FilesystemLoop is only available on nightly)
224
2
            let err = io::Error::other(format!("Path {:?} is a symlink", path));
225
2
            return Err(Error::io(err, &path, "metadata"));
226
20
        }
227

            
228
20
        if let Some(error) = self
229
20
            .verifier()
230
20
            .check_one(path.as_path(), PathType::Content, &meta)
231
20
            .into_iter()
232
20
            .next()
233
        {
234
            Err(error)
235
        } else {
236
20
            Ok(meta)
237
        }
238
1310
    }
239

            
240
    /// Create a [`Verifier`] with the appropriate rules for this
241
    /// `CheckedDir`.
242
215606
    pub fn verifier(&self) -> Verifier<'_> {
243
215606
        let mut v = self.mistrust.verifier();
244
215606
        if self.readable_okay {
245
2774
            v = v.permit_readable();
246
212832
        }
247
215606
        v
248
215606
    }
249

            
250
    /// Helper: Make sure that the path `p` is a relative path that can be
251
    /// guaranteed to stay within this directory.
252
    ///
253
    /// (Specifically, we reject absolute paths, ".." items, and Windows path prefixes.)
254
202894
    fn check_path(&self, p: &Path) -> Result<()> {
255
        use std::path::Component;
256
        // This check should be redundant, but let's be certain.
257
202894
        if p.is_absolute() {
258
79
            return Err(Error::InvalidSubdirectory);
259
202815
        }
260

            
261
445732
        for component in p.components() {
262
445732
            match component {
263
                Component::Prefix(_) | Component::RootDir | Component::ParentDir => {
264
152
                    return Err(Error::InvalidSubdirectory);
265
                }
266
445580
                Component::CurDir | Component::Normal(_) => {}
267
            }
268
        }
269

            
270
202663
        Ok(())
271
202894
    }
272

            
273
    /// Check whether `p` is a valid relative path within this directory,
274
    /// verify its permissions or the permissions of its parent, depending on `check_type`,
275
    /// and return an absolute path for `p`.
276
117095
    pub(crate) fn verified_full_path(
277
117095
        &self,
278
117095
        p: &Path,
279
117095
        check_type: FullPathCheck,
280
117095
    ) -> Result<PathBuf> {
281
117095
        self.check_path(p)?;
282
117087
        let full_path = self.location.join(p);
283
117087
        let to_verify: &Path = match check_type {
284
18475
            FullPathCheck::CheckPath => full_path.as_ref(),
285
98612
            FullPathCheck::CheckParent => full_path.parent().unwrap_or_else(|| full_path.as_ref()),
286
        };
287
117087
        self.verifier().check(to_verify)?;
288

            
289
113644
        Ok(full_path)
290
117095
    }
291
}
292

            
293
/// Type argument for [`CheckedDir::verified_full_path`].
294
#[derive(Clone, Copy, Debug)]
295
pub(crate) enum FullPathCheck {
296
    /// Check all elements of the path, including the final element.
297
    CheckPath,
298
    /// Check all elements of the path, not including the final element.
299
    CheckParent,
300
}
301

            
302
#[cfg(test)]
303
mod test {
304
    // @@ begin test lint list maintained by maint/add_warning @@
305
    #![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_time_subtraction)]
314
    #![allow(clippy::useless_vec)]
315
    #![allow(clippy::needless_pass_by_value)]
316
    #![allow(clippy::string_slice)] // See arti#2571
317
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
318
    use super::*;
319
    use crate::testing::Dir;
320
    use std::io::Write;
321

            
322
    #[test]
323
    fn easy_case() {
324
        let d = Dir::new();
325
        d.dir("a/b/c");
326
        d.dir("a/b/d");
327
        d.file("a/b/c/f1");
328
        d.file("a/b/c/f2");
329
        d.file("a/b/d/f3");
330

            
331
        d.chmod("a", 0o755);
332
        d.chmod("a/b", 0o700);
333
        d.chmod("a/b/c", 0o700);
334
        d.chmod("a/b/d", 0o777);
335
        d.chmod("a/b/c/f1", 0o600);
336
        d.chmod("a/b/c/f2", 0o666);
337
        d.chmod("a/b/d/f3", 0o600);
338

            
339
        let m = Mistrust::builder()
340
            .ignore_prefix(d.canonical_root())
341
            .build()
342
            .unwrap();
343

            
344
        let sd = m.verifier().secure_dir(d.path("a/b")).unwrap();
345

            
346
        // Try make_directory.
347
        sd.make_directory("c/sub1").unwrap();
348
        #[cfg(target_family = "unix")]
349
        {
350
            let e = sd.make_directory("d/sub2").unwrap_err();
351
            assert!(matches!(e, Error::BadPermission(..)));
352
        }
353

            
354
        // Try opening a file that exists.
355
        let f1 = sd.open("c/f1", OpenOptions::new().read(true)).unwrap();
356
        drop(f1);
357
        #[cfg(target_family = "unix")]
358
        {
359
            let e = sd.open("c/f2", OpenOptions::new().read(true)).unwrap_err();
360
            assert!(matches!(e, Error::BadPermission(..)));
361
            let e = sd.open("d/f3", OpenOptions::new().read(true)).unwrap_err();
362
            assert!(matches!(e, Error::BadPermission(..)));
363
        }
364

            
365
        // Try creating a file.
366
        let mut f3 = sd
367
            .open("c/f-new", OpenOptions::new().write(true).create(true))
368
            .unwrap();
369
        f3.write_all(b"Hello world").unwrap();
370
        drop(f3);
371

            
372
        #[cfg(target_family = "unix")]
373
        {
374
            let e = sd
375
                .open("d/f-new", OpenOptions::new().write(true).create(true))
376
                .unwrap_err();
377
            assert!(matches!(e, Error::BadPermission(..)));
378
        }
379
    }
380

            
381
    #[test]
382
    fn bad_paths() {
383
        let d = Dir::new();
384
        d.dir("a");
385
        d.chmod("a", 0o700);
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")).unwrap();
393

            
394
        let e = sd.make_directory("hello/../world").unwrap_err();
395
        assert!(matches!(e, Error::InvalidSubdirectory));
396
        let e = sd.metadata("hello/../world").unwrap_err();
397
        assert!(matches!(e, Error::InvalidSubdirectory));
398

            
399
        let e = sd.make_directory("/hello").unwrap_err();
400
        assert!(matches!(e, Error::InvalidSubdirectory));
401
        let e = sd.metadata("/hello").unwrap_err();
402
        assert!(matches!(e, Error::InvalidSubdirectory));
403

            
404
        sd.make_directory("hello/world").unwrap();
405
    }
406

            
407
    #[test]
408
    fn read_and_write() {
409
        let d = Dir::new();
410
        d.dir("a");
411
        d.chmod("a", 0o700);
412
        let m = Mistrust::builder()
413
            .ignore_prefix(d.canonical_root())
414
            .build()
415
            .unwrap();
416

            
417
        let checked = m.verifier().secure_dir(d.path("a")).unwrap();
418

            
419
        // Simple case: write and read.
420
        checked
421
            .write_and_replace("foo.txt", "this is incredibly silly")
422
            .unwrap();
423

            
424
        let s1 = checked.read_to_string("foo.txt").unwrap();
425
        let s2 = checked.read("foo.txt").unwrap();
426
        assert_eq!(s1, "this is incredibly silly");
427
        assert_eq!(s1.as_bytes(), &s2[..]);
428

            
429
        // Checked subdirectory
430
        let sub = "sub";
431
        let sub_checked = checked.make_secure_directory(sub).unwrap();
432
        assert_eq!(sub_checked.as_path(), checked.as_path().join(sub));
433

            
434
        // Trickier: write when the preferred temporary already has content.
435
        checked
436
            .open("bar.tmp", OpenOptions::new().create(true).write(true))
437
            .unwrap()
438
            .write_all("be the other guy".as_bytes())
439
            .unwrap();
440
        assert!(checked.join("bar.tmp").unwrap().try_exists().unwrap());
441

            
442
        checked
443
            .write_and_replace("bar.txt", "its hard and nobody understands")
444
            .unwrap();
445

            
446
        // Temp file should be gone.
447
        assert!(!checked.join("bar.tmp").unwrap().try_exists().unwrap());
448
        let s4 = checked.read_to_string("bar.txt").unwrap();
449
        assert_eq!(s4, "its hard and nobody understands");
450
    }
451

            
452
    #[test]
453
    fn read_directory() {
454
        let d = Dir::new();
455
        d.dir("a");
456
        d.chmod("a", 0o700);
457
        d.dir("a/b");
458
        d.file("a/b/f");
459
        d.file("a/c.d");
460
        d.dir("a/x");
461

            
462
        d.chmod("a", 0o700);
463
        d.chmod("a/b", 0o700);
464
        d.chmod("a/x", 0o777);
465
        let m = Mistrust::builder()
466
            .ignore_prefix(d.canonical_root())
467
            .build()
468
            .unwrap();
469

            
470
        let checked = m.verifier().secure_dir(d.path("a")).unwrap();
471

            
472
        assert!(matches!(
473
            checked.read_directory("/"),
474
            Err(Error::InvalidSubdirectory)
475
        ));
476
        assert!(matches!(
477
            checked.read_directory("b/.."),
478
            Err(Error::InvalidSubdirectory)
479
        ));
480
        let mut members: Vec<String> = checked
481
            .read_directory(".")
482
            .unwrap()
483
            .map(|ent| ent.unwrap().file_name().to_string_lossy().to_string())
484
            .collect();
485
        members.sort();
486
        assert_eq!(members, vec!["b", "c.d", "x"]);
487

            
488
        let members: Vec<String> = checked
489
            .read_directory("b")
490
            .unwrap()
491
            .map(|ent| ent.unwrap().file_name().to_string_lossy().to_string())
492
            .collect();
493
        assert_eq!(members, vec!["f"]);
494

            
495
        #[cfg(target_family = "unix")]
496
        {
497
            assert!(matches!(
498
                checked.read_directory("x"),
499
                Err(Error::BadPermission(_, _, _))
500
            ));
501
        }
502
    }
503

            
504
    #[test]
505
    fn remove_file() {
506
        let d = Dir::new();
507
        d.dir("a");
508
        d.chmod("a", 0o700);
509
        d.dir("a/b");
510
        d.file("a/b/f");
511
        d.dir("a/b/d");
512
        d.dir("a/x");
513
        d.dir("a/x/y");
514
        d.file("a/x/y/z");
515

            
516
        d.chmod("a", 0o700);
517
        d.chmod("a/b", 0o700);
518
        d.chmod("a/x", 0o777);
519

            
520
        let m = Mistrust::builder()
521
            .ignore_prefix(d.canonical_root())
522
            .build()
523
            .unwrap();
524
        let checked = m.verifier().secure_dir(d.path("a")).unwrap();
525

            
526
        // Remove a file that is there, and then make sure it is gone.
527
        assert!(checked.read_to_string("b/f").is_ok());
528
        assert!(checked.metadata("b/f").unwrap().is_file());
529
        checked.remove_file("b/f").unwrap();
530
        assert!(matches!(
531
            checked.read_to_string("b/f"),
532
            Err(Error::NotFound(_))
533
        ));
534
        assert!(matches!(checked.metadata("b/f"), Err(Error::NotFound(_))));
535
        assert!(matches!(
536
            checked.remove_file("b/f"),
537
            Err(Error::NotFound(_))
538
        ));
539

            
540
        // Remove a file in a nonexistent subdirectory
541
        assert!(matches!(
542
            checked.remove_file("b/xyzzy/fred"),
543
            Err(Error::NotFound(_))
544
        ));
545

            
546
        // Remove a file in a directory whose permissions are too open.
547
        #[cfg(target_family = "unix")]
548
        {
549
            assert!(matches!(
550
                checked.remove_file("x/y/z"),
551
                Err(Error::BadPermission(_, _, _))
552
            ));
553
            assert!(matches!(
554
                checked.metadata("x/y/z"),
555
                Err(Error::BadPermission(_, _, _))
556
            ));
557
        }
558
    }
559

            
560
    #[test]
561
    #[cfg(target_family = "unix")]
562
    fn access_symlink() {
563
        use crate::testing::LinkType;
564

            
565
        let d = Dir::new();
566
        d.dir("a/b");
567
        d.file("a/b/f1");
568

            
569
        d.chmod("a/b", 0o700);
570
        d.chmod("a/b/f1", 0o600);
571
        d.link_rel(LinkType::File, "f1", "a/b/f1-link");
572

            
573
        let m = Mistrust::builder()
574
            .ignore_prefix(d.canonical_root())
575
            .build()
576
            .unwrap();
577

            
578
        let sd = m.verifier().secure_dir(d.path("a/b")).unwrap();
579

            
580
        assert!(sd.open("f1", OpenOptions::new().read(true)).is_ok());
581

            
582
        // Metadata returns an error if called on a symlink
583
        let e = sd.metadata("f1-link").unwrap_err();
584
        assert!(
585
            matches!(e, Error::Io { ref err, .. } if err.to_string().contains("is a symlink")),
586
            "{e:?}"
587
        );
588

            
589
        // Open returns an error if called on a symlink.
590
        let e = sd
591
            .open("f1-link", OpenOptions::new().read(true))
592
            .unwrap_err();
593
        assert!(
594
            matches!(e, Error::Io { ref err, .. } if err.raw_os_error() == Some(libc::ELOOP)),
595
            "Expected ELOOP, but got: {e:?}"
596
        );
597
    }
598
}