4 from enum
import IntEnum
5 from pathlib
import Path
8 class Accessibility(IntEnum
):
10 The level of accessibility we have on a file or directory.
12 This is needed to assess the attack surface on the file system namespace we
13 have within a confined service. Higher levels mean more permissions for the
14 user and thus a bigger attack surface.
18 # Directories can be listed or files can be read.
21 # This is for special file systems such as procfs and for stuff such as
22 # FIFOs or character special files. The reason why this has a lower value
23 # than WRITABLE is because those files are more restricted on what and how
24 # they can be written to.
27 # Another special case are sticky directories, which do allow write access
28 # but restrict deletion. This does *not* apply to sticky directories that
32 # Essentially full permissions, the kind of accessibility we want to avoid
36 def assert_on(self
, path
: Path
) -> None:
38 Raise an AssertionError if the given 'path' allows for more
39 accessibility than 'self'.
44 actual
= self
.READABLE
48 dummy_file
= path
/ 'can_i_write'
52 if e
.errno
in [errno
.EROFS
, errno
.EACCES
]:
60 # The reason why we test this *after* we made sure it's
61 # writable is because we could have a sticky directory where
62 # the current user doesn't have write access.
63 if path
.stat().st_mode
& 0o1000 == 0o1000:
66 actual
= self
.WRITABLE
68 actual
= self
.READABLE
71 with path
.open('rb') as fp
:
73 actual
= self
.READABLE
74 except PermissionError
:
79 with path
.open('ab') as fp
:
83 except PermissionError
:
86 if e
.errno
== errno
.ETXTBSY
:
87 writable
= os
.access(path
, os
.W_OK
)
88 elif e
.errno
== errno
.EROFS
:
93 # Let's always try to fail towards being writable, so if *either*
94 # access(2) or a real write is successful it's writable. This is to
95 # make sure we don't accidentally introduce no-ops if we have bugs
96 # in the more complicated real write code above.
97 if writable
or os
.access(path
, os
.W_OK
):
98 actual
= self
.WRITABLE
100 # We need to be very careful when writing to or reading from
101 # special files (eg. FIFOs), since they can possibly block. So if
102 # it's not a file, just trust that access(2) won't lie.
103 if os
.access(path
, os
.R_OK
):
104 actual
= self
.READABLE
106 if os
.access(path
, os
.W_OK
):
107 actual
= self
.SPECIAL
111 details
= ', '.join([
112 f
'permissions: {stat.st_mode & 0o7777:o}',
113 f
'uid: {stat.st_uid}',
114 f
'group: {stat.st_gid}',
117 raise AssertionError(
118 f
'Expected at most {self!r} but got {actual!r} for path'
119 f
' {path} ({details}).'
123 def is_special_fs(path
: Path
) -> bool:
125 Check whether the given path truly is a special file system such as procfs
129 if path
== Path('/proc'):
130 return (path
/ 'version').read_text().startswith('Linux')
131 elif path
== Path('/sys'):
132 return b
'Linux' in (path
/ 'kernel' / 'notes').read_bytes()
133 except FileNotFoundError
:
138 def is_empty_dir(path
: Path
) -> bool:
142 except (StopIteration, PermissionError
):
146 def _assert_permissions_in_directory(
148 accessibility
: Accessibility
,
149 subdirs
: dict[Path
, Accessibility
],
151 accessibility
.assert_on(directory
)
153 for file in directory
.iterdir():
154 if is_special_fs(file):
155 msg
= f
'Got unexpected special filesystem at {file}.'
156 assert subdirs
.pop(file) == Accessibility
.SPECIAL
, msg
157 elif not file.is_symlink() and file.is_dir():
158 subdir_access
= subdirs
.pop(file, accessibility
)
159 if is_empty_dir(file):
160 # Whenever we got an empty directory, we check the permission
161 # constraints on the current directory (except if specified
162 # explicitly in subdirs) because for example if we're non-root
163 # (the constraints of the current directory are thus
164 # Accessibility.READABLE), we really have to make sure that
165 # empty directories are *never* writable.
166 subdir_access
.assert_on(file)
168 _assert_permissions_in_directory(file, subdir_access
, subdirs
)
170 subdirs
.pop(file, accessibility
).assert_on(file)
173 def assert_permissions(subdirs
: dict[str, Accessibility
]) -> None:
175 Recursively check whether the file system conforms to the accessibility
176 specification we specified via 'subdirs'.
179 absolute_subdirs
= {root
/ p
: a
for p
, a
in subdirs
.items()}
180 _assert_permissions_in_directory(
182 Accessibility
.WRITABLE
if os
.getuid() == 0 else Accessibility
.READABLE
,
185 for file in absolute_subdirs
.keys():
186 msg
= f
'Expected {file} to exist, but it was nowwhere to be found.'
187 raise AssertionError(msg
)