Skip to content

Commit a039111

Browse files
committed
fix: FileProvider.validate() leaked files outside root
Fixes two issues and adds two improvements: * FileProvider would read files outside the root if the root were a prefix of the file path string-wise. Consider a root path `/tmp/sample/root` and a relative path `../root.txt`, resulting in reading a file `/tmp/sample/root.txt`. * The "inside root" check was performed after checking if file existed. This would expose information about files outside the root through the exception. While an exploit would be difficult (if possible at all), it is better to fix this weakness anyway. * The exception type is changed from the most generic `Exception` to `ValueError` to make it easier to distinguish it from `ContentException` that is raised in other cases. * The `"/"` literal in path operations is changed to `os.sep` to improve portability. Signed-off-by: Jan Holeček <71874510+jholecek-rh@users.noreply.github.com>
1 parent 771b3ba commit a039111

File tree

2 files changed

+25
-10
lines changed

2 files changed

+25
-10
lines changed

insights/core/spec_factory.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,22 @@ def __init__(self, relative_path, root="/", save_as=None, ds=None, ctx=None, cle
214214

215215
self.validate()
216216

217+
def _is_inside_root(self):
218+
"""Checks that `self.relative_path` does not point outside `self.root`."""
219+
resolved = os.path.realpath(self.path)
220+
221+
# pathlib.Path.is_relative_to() has been added only in Python 3.9
222+
resolved_root = os.path.realpath(self.root)
223+
if not resolved_root.endswith(os.sep):
224+
resolved_root += os.sep
225+
226+
return resolved.startswith(resolved_root)
227+
217228
def validate(self):
229+
if not self._is_inside_root():
230+
msg = "Relative path points outside the root: %s"
231+
raise ValueError(msg % (self.path))
232+
218233
# 1. No Such File
219234
if not os.path.exists(self.path):
220235
raise ContentException("%s does not exist." % self.path)
@@ -224,15 +239,10 @@ def validate(self):
224239
if self._filterable and not self._filters:
225240
raise NoFilterException("Skipping %s due to no filters." % dr.get_name(self.ds))
226241
# 2.2 Customer Prohibits Collection
227-
if not blacklist.allow_file("/" + self.relative_path):
228-
log.warning("WARNING: Skipping file %s", "/" + self.relative_path)
242+
if not blacklist.allow_file(os.sep + self.relative_path):
243+
log.warning("WARNING: Skipping file %s", os.sep + self.relative_path)
229244
raise BlacklistedSpec()
230245

231-
resolved = os.path.realpath(self.path)
232-
if not resolved.startswith(os.path.realpath(self.root)):
233-
msg = "Relative path points outside the root: %s -> %s."
234-
raise Exception(msg % (self.path, resolved))
235-
236246
if not os.access(self.path, os.R_OK):
237247
raise ContentException("Cannot access %s" % self.path)
238248

insights/tests/core/spec_factory/test_file_provider.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ def create_file(relpath, content=""):
3232
create_file(SAMPLE_FILE)
3333
create_directory("sample/root")
3434
create_file("sample/root/access_denied.txt")
35-
os.chmod(os.path.join(root, "sample/root/access_denied.txt"), 0)
35+
os.chmod(os.path.join(root, "sample/root/access_denied.txt"), 0o000)
36+
create_file("sample/root.txt")
3637
return root
3738

3839

@@ -48,7 +49,7 @@ def test_repr(sample_directory):
4849

4950

5051
@pytest.mark.skipif(
51-
# GitHub workflows run Python 2.7 and 3.6 tests in containers, because these old Python
52+
# GitHub workflows run Python 2.7 and 3.6 tests in containers because these old Python
5253
# versions are not available in GitHub-hosted runners anymore. The tests are executed as root.
5354
os.getuid() == 0,
5455
reason="Test must not be run as root. Root ignores file mode bits."
@@ -59,7 +60,11 @@ def test_repr(sample_directory):
5960
("access_denied.txt", ContentException, "Cannot access"),
6061
("no_such_file.txt", ContentException, "does not exist"),
6162
# file outside root that exists
62-
("../../sample_file.txt", Exception, "Relative path points outside the root"),
63+
("../../sample_file.txt", ValueError, "Relative path points outside the root"),
64+
# file outside root that does not exist
65+
("../no_such_file.txt", ValueError, "Relative path points outside the root"),
66+
# file outside root where root path is a prefix of the file path string-wise
67+
("../root.txt", ValueError, "Relative path points outside the root"),
6368
]
6469
)
6570
def test_validate(sample_directory, relpath, exception_type, match):

0 commit comments

Comments
 (0)