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 Error, Result, Type,
13 walk::{PathType, ResolvePath},
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> + '_ + use<'_> {
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(
89 &self,
90 path: &Path,
91 ) -> impl Iterator<Item = Error> + '_ + use<'_> {
92 use std::sync::Arc;
93
94 if !self.check_contents || self.mistrust.is_disabled() {
95 return boxed(std::iter::empty());
96 }
97
98 boxed(
99 walkdir::WalkDir::new(path)
100 .follow_links(false)
101 .min_depth(1)
102 .into_iter()
103 .flat_map(move |ent| match ent {
104 Err(err) => vec![Error::Listing(Arc::new(err))],
105 Ok(ent) => match ent.metadata() {
106 Ok(meta) => self
107 .check_one(ent.path(), PathType::Content, &meta)
108 .into_iter()
109 .map(|e| Error::Content(Box::new(e)))
110 .collect(),
111 Err(err) => vec![Error::Listing(Arc::new(err))],
112 },
113 }),
114 )
115 }
116
117 /// Return an empty iterator.
118 #[cfg(not(feature = "walkdir"))]
119 pub(crate) fn check_content_errors(&self, _path: &Path) -> impl Iterator<Item = Error> + '_ {
120 std::iter::empty()
121 }
122
123 /// Check a single `path` for conformance with this `Verifier`.
124 ///
125 /// Note that this result is only meaningful if all of the _ancestors_ of
126 /// this path have been checked. Otherwise, a non-trusted user could change
127 /// where this path points after it has been checked.
128 #[must_use]
129 pub(crate) fn check_one(
130 &self,
131 path: &Path,
132 path_type: PathType,
133 meta: &Metadata,
134 ) -> Vec<Error> {
135 let mut errors = Vec::new();
136
137 self.check_type(path, path_type, meta, &mut errors);
138 #[cfg(target_family = "unix")]
139 self.check_permissions(path, path_type, meta, &mut errors);
140 errors
141 }
142
143 /// Check whether a given file has the correct type, and push an error into
144 /// `errors` if not. Other inputs are as for `check_one`.
145 fn check_type(
146 &self,
147 path: &Path,
148 path_type: PathType,
149 meta: &Metadata,
150 errors: &mut Vec<Error>,
151 ) {
152 let want_type = match path_type {
153 PathType::Symlink => {
154 // There's nothing to check on a symlink encountered _while
155 // looking up the target_; its permissions and ownership do not
156 // actually matter.
157 return;
158 }
159 PathType::Intermediate => Type::Dir,
160 PathType::Final => self.enforce_type,
161 PathType::Content => Type::DirOrFile,
162 };
163
164 if !want_type.matches(meta.file_type()) {
165 errors.push(Error::BadType(path.into()));
166 }
167 }
168
169 /// Check whether a given file has the correct ownership and permissions,
170 /// and push errors into `errors` if not. Other inputs are as for
171 /// `check_one`.
172 ///
173 /// On iOS, check permissions but assumes the owner is the current user.
174 #[cfg(target_family = "unix")]
175 fn check_permissions(
176 &self,
177 path: &Path,
178 path_type: PathType,
179 meta: &Metadata,
180 errors: &mut Vec<Error>,
181 ) {
182 // We need to check that the owner is trusted, since the owner can
183 // always change the permissions of the object. (If we're talking
184 // about a directory, the owner cah change the permissions and owner
185 // of anything in the directory.)
186
187 #[cfg(all(
188 not(target_os = "ios"),
189 not(target_os = "tvos"),
190 not(target_os = "android")
191 ))]
192 {
193 let uid = meta.uid();
194 if uid != 0 && Some(uid) != self.mistrust.trust_user {
195 errors.push(Error::BadOwner(path.into(), uid));
196 }
197 }
198
199 // On Unix-like platforms, symlink permissions are ignored (and usually
200 // not settable). Theoretically, the symlink owner shouldn't matter, but
201 // it's less confusing to consistently require the right owner.
202 if path_type == PathType::Symlink {
203 return;
204 }
205
206 let mut forbidden_bits = if !self.readable_okay && path_type == PathType::Final {
207 // If this is the target object, and it must not be readable, then
208 // we forbid it to be group-rwx and all-rwx.
209 //
210 // (We allow _content_ to be globally readable even if readable_okay
211 // is false, since we check that the Final directory is itself
212 // unreadable. This is okay unless the content has hard links: see
213 // the Limitations section of the crate-level documentation.)
214 0o077
215 } else {
216 // If this is the target object and it may be readable, or if this
217 // is _any parent directory_ or any content, then we typically
218 // forbid the group-write and all-write bits. (Those are the bits
219 // that would allow non-trusted users to change the object, or
220 // change things around in a directory.)
221 if meta.is_dir() && meta.mode() & STICKY_BIT != 0 && path_type == PathType::Intermediate
222 {
223 // This is an intermediate directory and this sticky bit is
224 // set. Thus, we don't care if it is world-writable or
225 // group-writable, since only the _owner_ of a file in this
226 // directory can move or rename it.
227 0o000
228 } else {
229 // It's not a sticky-bit intermediate directory; actually
230 // forbid 022.
231 0o022
232 }
233 };
234 // If we trust the GID, then we allow even more bits to be set.
235 #[cfg(all(
236 not(target_os = "ios"),
237 not(target_os = "tvos"),
238 not(target_os = "android")
239 ))]
240 if self.mistrust.trust_group == Some(meta.gid()) {
241 forbidden_bits &= !0o070;
242 }
243
244 // Both iOS and Android have some directory on the path for application data directory
245 // which is group writeable. However both system already offer some guarantees regarding
246 // application data being kept away from other apps.
247 //
248 // iOS: https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html
249 // > For security purposes, an iOS app’s interactions with the file system are limited
250 // to the directories inside the app’s sandbox directory
251 //
252 // Android: https://developer.android.com/training/data-storage
253 // > App-specific storage: [...] Use the directories within internal storage to save
254 // sensitive information that other apps shouldn't access.
255 #[cfg(any(target_os = "ios", target_os = "tvos", target_os = "android"))]
256 {
257 forbidden_bits &= !0o070;
258 }
259
260 let bad_bits = meta.mode() & forbidden_bits;
261 if bad_bits != 0 {
262 errors.push(Error::BadPermission(
263 path.into(),
264 meta.mode() & 0o777,
265 bad_bits,
266 ));
267 }
268 }
269}
270
271impl super::Type {
272 /// Return true if this required type is matched by a given `FileType`
273 /// object.
274 fn matches(&self, have_type: FileType) -> bool {
275 match self {
276 Type::Dir => have_type.is_dir(),
277 Type::File => have_type.is_file(),
278 Type::DirOrFile => have_type.is_dir() || have_type.is_file(),
279 Type::Anything => true,
280 }
281 }
282}