fs_mistrust/
imp.rs

1//! Implementation logic for `fs-mistrust`.
2
3use std::{
4    fs::{FileType, Metadata},
5    path::Path,
6};
7
8#[cfg(target_family = "unix")]
9use std::os::unix::prelude::MetadataExt;
10
11use crate::{
12    walk::{PathType, ResolvePath},
13    Error, Result, Type,
14};
15
16/// Definition for the "sticky bit", which on Unix means that the contents of
17/// directory may not be renamed, deleted, or otherwise modified by a non-owner
18/// of those contents, even if the user has write permissions on the directory.
19///
20/// This is the usual behavior for /tmp: You can make your own directories in
21/// /tmp, but you can't modify other people's.
22///
23/// (We'd use libc's version of `S_ISVTX`, but they vacillate between u16 and
24/// u32 depending what platform you're on.)
25#[cfg(target_family = "unix")]
26pub(crate) const STICKY_BIT: u32 = 0o1000;
27
28/// Helper: Box an iterator of errors.
29fn boxed<'a, I: Iterator<Item = Error> + 'a>(iter: I) -> Box<dyn Iterator<Item = Error> + 'a> {
30    Box::new(iter)
31}
32
33impl<'a> super::Verifier<'a> {
34    /// Return an iterator of all the security problems with `path`.
35    ///
36    /// If the iterator is empty, then there is no problem with `path`.
37    //
38    // TODO: This iterator is not fully lazy; sometimes, calls to check_one()
39    // return multiple errors when it would be better for them to return only
40    // one (since we're ignoring errors after the first).  This might be nice
41    // to fix in the future if we can do so without adding much complexity
42    // to the code.  It's not urgent, since the allocations won't cost much
43    // compared to the filesystem access.
44    pub(crate) fn check_errors(&self, path: &Path) -> impl Iterator<Item = Error> + '_ {
45        if self.mistrust.is_disabled() {
46            // We don't want to walk the path in this case at all: we'll just
47            // look at the last element.
48
49            let meta = match path.metadata() {
50                Ok(meta) => meta,
51                Err(e) => return boxed(vec![Error::inspecting(e, path)].into_iter()),
52            };
53            let mut errors = Vec::new();
54            self.check_type(path, PathType::Final, &meta, &mut errors);
55            return boxed(errors.into_iter());
56        }
57
58        let rp = match ResolvePath::new(path) {
59            Ok(rp) => rp,
60            Err(e) => return boxed(vec![e].into_iter()),
61        };
62
63        // Filter to remove every path that is a prefix of ignore_prefix. (IOW,
64        // if stop_at_dir is /home/arachnidsGrip, real_stop_at_dir will be
65        // /home, and we'll ignore / and /home.)
66        let should_retain = move |r: &Result<_>| match (r, &self.mistrust.ignore_prefix) {
67            (Ok((p, _, _)), Some(ignore_prefix)) => !ignore_prefix.starts_with(p),
68            (_, _) => true,
69        };
70
71        boxed(
72            rp.filter(should_retain)
73                // Finally, check the path for errors.
74                //
75                // See `check_one` below for a note on TOCTOU issues.
76                .flat_map(move |r| match r {
77                    Ok((path, path_type, metadata)) => {
78                        self.check_one(path.as_path(), path_type, &metadata)
79                    }
80                    Err(e) => vec![e],
81                }),
82        )
83    }
84
85    /// If check_contents is set, return an iterator over all the errors in
86    /// elements _contained in this directory_.
87    #[cfg(feature = "walkdir")]
88    pub(crate) fn check_content_errors(&self, path: &Path) -> impl Iterator<Item = Error> + '_ {
89        use std::sync::Arc;
90
91        if !self.check_contents || self.mistrust.is_disabled() {
92            return boxed(std::iter::empty());
93        }
94
95        boxed(
96            walkdir::WalkDir::new(path)
97                .follow_links(false)
98                .min_depth(1)
99                .into_iter()
100                .flat_map(move |ent| match ent {
101                    Err(err) => vec![Error::Listing(Arc::new(err))],
102                    Ok(ent) => match ent.metadata() {
103                        Ok(meta) => self
104                            .check_one(ent.path(), PathType::Content, &meta)
105                            .into_iter()
106                            .map(|e| Error::Content(Box::new(e)))
107                            .collect(),
108                        Err(err) => vec![Error::Listing(Arc::new(err))],
109                    },
110                }),
111        )
112    }
113
114    /// Return an empty iterator.
115    #[cfg(not(feature = "walkdir"))]
116    pub(crate) fn check_content_errors(&self, _path: &Path) -> impl Iterator<Item = Error> + '_ {
117        std::iter::empty()
118    }
119
120    /// Check a single `path` for conformance with this `Verifier`.
121    ///
122    /// Note that this result is only meaningful if all of the _ancestors_ of
123    /// this path have been checked.  Otherwise, a non-trusted user could change
124    /// where this path points after it has been checked.
125    #[must_use]
126    pub(crate) fn check_one(
127        &self,
128        path: &Path,
129        path_type: PathType,
130        meta: &Metadata,
131    ) -> Vec<Error> {
132        let mut errors = Vec::new();
133
134        self.check_type(path, path_type, meta, &mut errors);
135        #[cfg(target_family = "unix")]
136        self.check_permissions(path, path_type, meta, &mut errors);
137        errors
138    }
139
140    /// Check whether a given file has the correct type, and push an error into
141    /// `errors` if not. Other inputs are as for `check_one`.
142    fn check_type(
143        &self,
144        path: &Path,
145        path_type: PathType,
146        meta: &Metadata,
147        errors: &mut Vec<Error>,
148    ) {
149        let want_type = match path_type {
150            PathType::Symlink => {
151                // There's nothing to check on a symlink encountered _while
152                // looking up the target_; its permissions and ownership do not
153                // actually matter.
154                return;
155            }
156            PathType::Intermediate => Type::Dir,
157            PathType::Final => self.enforce_type,
158            PathType::Content => Type::DirOrFile,
159        };
160
161        if !want_type.matches(meta.file_type()) {
162            errors.push(Error::BadType(path.into()));
163        }
164    }
165
166    /// Check whether a given file has the correct ownership and permissions,
167    /// and push errors into `errors` if not. Other inputs are as for
168    /// `check_one`.
169    ///
170    /// On iOS, check permissions but assumes the owner is the current user.
171    #[cfg(target_family = "unix")]
172    fn check_permissions(
173        &self,
174        path: &Path,
175        path_type: PathType,
176        meta: &Metadata,
177        errors: &mut Vec<Error>,
178    ) {
179        // We need to check that the owner is trusted, since the owner can
180        // always change the permissions of the object.  (If we're talking
181        // about a directory, the owner cah change the permissions and owner
182        // of anything in the directory.)
183
184        #[cfg(all(
185            not(target_os = "ios"),
186            not(target_os = "tvos"),
187            not(target_os = "android")
188        ))]
189        {
190            let uid = meta.uid();
191            if uid != 0 && Some(uid) != self.mistrust.trust_user {
192                errors.push(Error::BadOwner(path.into(), uid));
193            }
194        }
195
196        // On Unix-like platforms, symlink permissions are ignored (and usually
197        // not settable). Theoretically, the symlink owner shouldn't matter, but
198        // it's less confusing to consistently require the right owner.
199        if path_type == PathType::Symlink {
200            return;
201        }
202
203        let mut forbidden_bits = if !self.readable_okay && path_type == PathType::Final {
204            // If this is the target object, and it must not be readable, then
205            // we forbid it to be group-rwx and all-rwx.
206            //
207            // (We allow _content_ to be globally readable even if readable_okay
208            // is false, since we check that the Final directory is itself
209            // unreadable.  This is okay unless the content has hard links: see
210            // the Limitations section of the crate-level documentation.)
211            0o077
212        } else {
213            // If this is the target object and it may be readable, or if this
214            // is _any parent directory_ or any content, then we typically
215            // forbid the group-write and all-write bits.  (Those are the bits
216            // that would allow non-trusted users to change the object, or
217            // change things around in a directory.)
218            if meta.is_dir() && meta.mode() & STICKY_BIT != 0 && path_type == PathType::Intermediate
219            {
220                // This is an intermediate directory and this sticky bit is
221                // set.  Thus, we don't care if it is world-writable or
222                // group-writable, since only the _owner_  of a file in this
223                // directory can move or rename it.
224                0o000
225            } else {
226                // It's not a sticky-bit intermediate directory; actually
227                // forbid 022.
228                0o022
229            }
230        };
231        // If we trust the GID, then we allow even more bits to be set.
232        #[cfg(all(
233            not(target_os = "ios"),
234            not(target_os = "tvos"),
235            not(target_os = "android")
236        ))]
237        if self.mistrust.trust_group == Some(meta.gid()) {
238            forbidden_bits &= !0o070;
239        }
240
241        // Both iOS and Android have some directory on the path for application data directory
242        // which is group writeable. However both system already offer some guarantees regarding
243        // application data being kept away from other apps.
244        //
245        // iOS: https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html
246        // > For security purposes, an iOS app’s interactions with the file system are limited
247        // to the directories inside the app’s sandbox directory
248        //
249        // Android: https://developer.android.com/training/data-storage
250        // > App-specific storage: [...] Use the directories within internal storage to save
251        // sensitive information that other apps shouldn't access.
252        #[cfg(any(target_os = "ios", target_os = "tvos", target_os = "android"))]
253        {
254            forbidden_bits &= !0o070;
255        }
256
257        let bad_bits = meta.mode() & forbidden_bits;
258        if bad_bits != 0 {
259            errors.push(Error::BadPermission(
260                path.into(),
261                meta.mode() & 0o777,
262                bad_bits,
263            ));
264        }
265    }
266}
267
268impl super::Type {
269    /// Return true if this required type is matched by a given `FileType`
270    /// object.
271    fn matches(&self, have_type: FileType) -> bool {
272        match self {
273            Type::Dir => have_type.is_dir(),
274            Type::File => have_type.is_file(),
275            Type::DirOrFile => have_type.is_dir() || have_type.is_file(),
276            Type::Anything => true,
277        }
278    }
279}