1
//! Functionality for opening files while verifying their permissions.
2

            
3
use std::{
4
    borrow::Cow,
5
    fs::{File, OpenOptions},
6
    io::{Read as _, Write},
7
    path::{Path, PathBuf},
8
};
9

            
10
#[cfg(unix)]
11
use std::os::unix::fs::OpenOptionsExt as _;
12

            
13
use 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.
30
pub 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.
41
pub(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

            
48
impl<'a> FileAccess<'a> {
49
    /// Create a new `FileAccess` to access files within CheckedDir,
50
    /// using default options.
51
28374
    pub(crate) fn from_checked_dir(checked_dir: &'a CheckedDir) -> Self {
52
28374
        Self::from_inner(Inner::CheckedDir(checked_dir))
53
28374
    }
54
    /// Create a new `FileAccess` to access files anywhere on the filesystem,
55
    /// using default options.
56
630
    pub(crate) fn from_verifier(verifier: Verifier<'a>) -> Self {
57
630
        Self::from_inner(Inner::Verifier(verifier))
58
630
    }
59
    /// Create a new `FileAccess` from `inner`,
60
    /// using default options.
61
29004
    fn from_inner(inner: Inner<'a>) -> Self {
62
29004
        Self {
63
29004
            inner,
64
29004
            #[cfg(unix)]
65
29004
            create_with_mode: None,
66
29004
            follow_final_links: false,
67
29004
        }
68
29004
    }
69
    /// Check path constraints on `path` and verify its permissions
70
    /// (or the permissions of its parent) according to `check_type`
71
43257
    fn verified_full_path(&self, path: &Path, check_type: FullPathCheck) -> Result<PathBuf> {
72
43257
        match &self.inner {
73
42627
            Inner::CheckedDir(cd) => cd.verified_full_path(path, check_type),
74
630
            Inner::Verifier(v) => {
75
630
                let to_verify = match check_type {
76
626
                    FullPathCheck::CheckPath => path,
77
4
                    FullPathCheck::CheckParent => path.parent().unwrap_or(path),
78
                };
79
630
                v.check(to_verify)?;
80
318
                Ok(path.into())
81
            }
82
        }
83
43257
    }
84
    /// Return a `Verifier` to use for checking permissions.
85
23065
    fn verifier(&self) -> crate::Verifier {
86
23065
        match &self.inner {
87
22591
            Inner::CheckedDir(cd) => cd.verifier(),
88
474
            Inner::Verifier(v) => v.clone(),
89
        }
90
23065
    }
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
14411
    fn location_unverified<'b>(&self, path: &'b Path) -> Result<Cow<'b, Path>> {
96
14411
        Ok(match self.inner {
97
14253
            Inner::CheckedDir(cd) => cd.join(path)?.into(),
98
158
            Inner::Verifier(_) => path.into(),
99
        })
100
14411
    }
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
4
    pub fn create_with_mode(mut self, mode: u32) -> Self {
109
4
        #[cfg(unix)]
110
4
        {
111
4
            self.create_with_mode = Some(mode);
112
4
        }
113
4
        self
114
4
    }
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
1242
    pub fn follow_final_links(mut self, follow: bool) -> Self {
132
1242
        self.follow_final_links = follow;
133
1242
        self
134
1242
    }
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
4340
    pub fn open<P: AsRef<Path>>(self, path: P, options: &OpenOptions) -> Result<File> {
151
4340
        self.open_internal(path.as_ref(), options)
152
4340
    }
153

            
154
    /// As `open`, but take `self` by reference.  For internal use.
155
29004
    fn open_internal(&self, path: &Path, options: &OpenOptions) -> Result<File> {
156
29004
        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
29004
        let check_type = if follow_links {
162
1242
            FullPathCheck::CheckPath
163
        } else {
164
27762
            FullPathCheck::CheckParent
165
        };
166

            
167
29004
        let path = match self.verified_full_path(path.as_ref(), check_type) {
168
27148
            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
158
            Err(Error::NotFound(_)) if follow_links => self.location_unverified(path.as_ref())?,
175
1698
            Err(e) => return Err(e),
176
        };
177

            
178
        #[allow(unused_mut)]
179
27306
        let mut options = options.clone();
180
27306

            
181
27306
        #[cfg(unix)]
182
27306
        {
183
27306
            let create_mode = self.create_with_mode.unwrap_or(0o600);
184
27306
            options.mode(create_mode);
185
27306
            // Don't follow symlinks out of a secure directory.
186
27306
            if !follow_links {
187
26295
                options.custom_flags(libc::O_NOFOLLOW);
188
26295
            }
189
        }
190

            
191
27306
        let file = options
192
27306
            .open(&path)
193
27364
            .map_err(|e| Error::io(e, path.as_ref(), "open file"))?;
194
23065
        let meta = file
195
23065
            .metadata()
196
23065
            .map_err(|e| Error::inspecting(e, path.as_ref()))?;
197

            
198
23065
        if let Some(error) = self
199
23065
            .verifier()
200
23065
            .check_one(path.as_ref(), PathType::Content, &meta)
201
23065
            .into_iter()
202
23065
            .next()
203
        {
204
79
            Err(error)
205
        } else {
206
22986
            Ok(file)
207
        }
208
29004
    }
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
1726
    pub fn read_to_string<P: AsRef<Path>>(self, path: P) -> Result<String> {
217
1726
        let path = path.as_ref();
218
1726
        let mut file = self.open(path, OpenOptions::new().read(true))?;
219
1022
        let mut result = String::new();
220
1022
        file.read_to_string(&mut result)
221
1022
            .map_err(|e| Error::io(e, path, "read file"))?;
222
1020
        Ok(result)
223
1726
    }
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
2560
    pub fn read<P: AsRef<Path>>(self, path: P) -> Result<Vec<u8>> {
231
2560
        let path = path.as_ref();
232
2560
        let mut file = self.open(path, OpenOptions::new().read(true))?;
233
748
        let mut result = Vec::new();
234
748
        file.read_to_end(&mut result)
235
748
            .map_err(|e| Error::io(e, path, "read file"))?;
236
748
        Ok(result)
237
2560
    }
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
1708
    pub fn write_and_replace<P: AsRef<Path>, C: AsRef<[u8]>>(
256
1708
        self,
257
1708
        path: P,
258
1708
        contents: C,
259
1708
    ) -> Result<()> {
260
1708
        let path = path.as_ref();
261
1708
        let final_path = self.verified_full_path(path, FullPathCheck::CheckParent)?;
262

            
263
1708
        let tmp_name = path.with_extension("tmp");
264
1708
        // We remove the temporary file before opening it, if it's present: otherwise it _might_ be
265
1708
        // a symlink to somewhere silly.
266
1708
        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
1708
        let mut tmp_file = self.open_internal(
271
1708
            &tmp_name,
272
1708
            OpenOptions::new().create(true).truncate(true).write(true),
273
1708
        )?;
274

            
275
        // Write the data.
276
1708
        tmp_file
277
1708
            .write_all(contents.as_ref())
278
1708
            .map_err(|e| Error::io(e, &tmp_name, "write to file"))?;
279
        // Flush and close.
280
1708
        drop(tmp_file);
281
1708

            
282
1708
        // Replace the old file.
283
1708
        std::fs::rename(
284
1708
            // It's okay to use location_unverified here, since we already verified it when we
285
1708
            // called `open`.
286
1708
            self.location_unverified(tmp_name.as_path())?,
287
1708
            final_path,
288
1708
        )
289
1708
        .map_err(|e| Error::io(e, path, "replace file"))?;
290
1708
        Ok(())
291
1708
    }
292
}
293

            
294
#[cfg(test)]
295
mod 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_duration_subtraction)]
306
    #![allow(clippy::useless_vec)]
307
    #![allow(clippy::needless_pass_by_value)]
308
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
309

            
310
    #[cfg(unix)]
311
    use std::fs;
312

            
313
    use super::*;
314
    use crate::{Mistrust, testing::Dir};
315

            
316
    #[test]
317
    fn create_public_in_checked_dir() {
318
        let d = Dir::new();
319
        d.dir("a");
320

            
321
        d.chmod("a", 0o700);
322

            
323
        let m = Mistrust::builder()
324
            .ignore_prefix(d.canonical_root())
325
            .build()
326
            .unwrap();
327
        let checked = m.verifier().secure_dir(d.path("a")).unwrap();
328

            
329
        {
330
            let mut f = checked
331
                .file_access()
332
                .open(
333
                    "private-1.txt",
334
                    OpenOptions::new().write(true).create_new(true),
335
                )
336
                .unwrap();
337
            f.write_all(b"Hello world\n").unwrap();
338

            
339
            checked
340
                .file_access()
341
                .write_and_replace("private-2.txt", b"Hello world 2\n")
342
                .unwrap();
343
        }
344
        {
345
            let mut f = checked
346
                .file_access()
347
                .create_with_mode(0o640)
348
                .open(
349
                    "public-1.txt",
350
                    OpenOptions::new().write(true).create_new(true),
351
                )
352
                .unwrap();
353
            f.write_all(b"Hello wider world\n").unwrap();
354

            
355
            checked
356
                .file_access()
357
                .create_with_mode(0o644)
358
                .write_and_replace("public-2.txt", b"Hello wider world 2")
359
                .unwrap();
360
        }
361

            
362
        #[cfg(target_family = "unix")]
363
        {
364
            use std::os::unix::fs::MetadataExt;
365
            assert_eq!(
366
                fs::metadata(d.path("a/private-1.txt")).unwrap().mode() & 0o7777,
367
                0o600
368
            );
369
            assert_eq!(
370
                fs::metadata(d.path("a/private-2.txt")).unwrap().mode() & 0o7777,
371
                0o600
372
            );
373
            assert_eq!(
374
                fs::metadata(d.path("a/public-1.txt")).unwrap().mode() & 0o7777,
375
                0o640
376
            );
377
            assert_eq!(
378
                fs::metadata(d.path("a/public-2.txt")).unwrap().mode() & 0o7777,
379
                0o644
380
            );
381
        }
382
    }
383

            
384
    #[test]
385
    #[cfg(unix)]
386
    fn open_symlinks() {
387
        use crate::testing::LinkType;
388
        let d = Dir::new();
389
        d.dir("a");
390
        d.dir("a/b");
391
        d.dir("a/c");
392
        d.file("a/c/file1.txt");
393
        d.link_rel(LinkType::File, "../c/file1.txt", "a/b/present");
394
        d.link_rel(LinkType::File, "../c/file2.txt", "a/b/absent");
395
        d.chmod("a", 0o700);
396
        d.chmod("a/b", 0o700);
397
        d.chmod("a/c", 0o700);
398
        d.chmod("a/c/file1.txt", 0o600);
399

            
400
        let m = Mistrust::builder()
401
            .ignore_prefix(d.canonical_root())
402
            .build()
403
            .unwrap();
404

            
405
        // Try reading
406
        let contents = m
407
            .file_access()
408
            .follow_final_links(true)
409
            .read(d.path("a/b/present"))
410
            .unwrap();
411
        assert_eq!(
412
            &contents[..],
413
            &b"This space is intentionally left blank"[..]
414
        );
415
        let error = m
416
            .file_access()
417
            .follow_final_links(true)
418
            .read(d.path("a/b/absent"))
419
            .unwrap_err();
420
        assert!(matches!(error, Error::NotFound(_)));
421

            
422
        // Try writing.
423
        {
424
            let mut f = m
425
                .file_access()
426
                .follow_final_links(true)
427
                .open(
428
                    d.path("a/b/present"),
429
                    OpenOptions::new().write(true).truncate(true),
430
                )
431
                .unwrap();
432
            f.write_all(b"This is extremely serious!").unwrap();
433
        }
434
        let contents = m
435
            .file_access()
436
            .follow_final_links(true)
437
            .read(d.path("a/b/present"))
438
            .unwrap();
439
        assert_eq!(&contents[..], &b"This is extremely serious!"[..]);
440

            
441
        let contents = m.file_access().read(d.path("a/c/file1.txt")).unwrap();
442
        assert_eq!(&contents[..], &b"This is extremely serious!"[..]);
443
        {
444
            let mut f = m
445
                .file_access()
446
                .follow_final_links(true)
447
                .open(
448
                    d.path("a/b/absent"),
449
                    OpenOptions::new().create(true).write(true),
450
                )
451
                .unwrap();
452
            f.write_all(b"This is extremely silly!").unwrap();
453
        }
454
        let contents = m.file_access().read(d.path("a/c/file2.txt")).unwrap();
455
        assert_eq!(&contents[..], &b"This is extremely silly!"[..]);
456
    }
457
}