Skip to content

Commit 1bb158d

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 6a819a7 commit 1bb158d

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
@@ -215,7 +215,22 @@ def __init__(self, relative_path, root="/", save_as=None, ds=None, ctx=None, cle
215215

216216
self.validate()
217217

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

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

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)