Skip to content

Commit 79fbf80

Browse files
minrkCarreauwillingc
authored
Merge pull request from GHSA-hrw6-wg82-cm62
* filefind: avoid handling absolute paths we don't need or want absolute path support, which we inherited from generic ipython_genutils only supporting relative paths lets us avoid attempting to accessing files we know we won't accept * Apply suggestions from code review Co-authored-by: M Bussonnier <[email protected]> * filefind: only accept Sequence[str] we only call it one place, might as well be simple about it * version_info gate for is_relative_to * clarify docstring Co-authored-by: Carol Willing <[email protected]> --------- Co-authored-by: M Bussonnier <[email protected]> Co-authored-by: Carol Willing <[email protected]>
1 parent f137916 commit 79fbf80

File tree

2 files changed

+81
-57
lines changed

2 files changed

+81
-57
lines changed

jupyter_server/utils.py

Lines changed: 42 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import sys
1212
import warnings
1313
from contextlib import contextmanager
14+
from pathlib import Path
1415
from typing import Any, Generator, NewType, Sequence
1516
from urllib.parse import (
1617
SplitResult,
@@ -338,81 +339,65 @@ def is_namespace_package(namespace: str) -> bool | None:
338339
return isinstance(spec.submodule_search_locations, _NamespacePath)
339340

340341

341-
def filefind(filename: str, path_dirs: Sequence[str] | str | None = None) -> str:
342+
def filefind(filename: str, path_dirs: Sequence[str]) -> str:
342343
"""Find a file by looking through a sequence of paths.
343-
This iterates through a sequence of paths looking for a file and returns
344-
the full, absolute path of the first occurrence of the file. If no set of
345-
path dirs is given, the filename is tested as is, after running through
346-
:func:`expandvars` and :func:`expanduser`. Thus a simple call::
347344
348-
filefind("myfile.txt")
345+
For use in FileFindHandler.
349346
350-
will find the file in the current working dir, but::
347+
Iterates through a sequence of paths looking for a file and returns
348+
the full, absolute path of the first occurrence of the file.
351349
352-
filefind("~/myfile.txt")
350+
Absolute paths are not accepted for inputs.
353351
354-
Will find the file in the users home directory. This function does not
355-
automatically try any paths, such as the cwd or the user's home directory.
352+
This function does not automatically try any paths,
353+
such as the cwd or the user's home directory.
356354
357355
Parameters
358356
----------
359357
filename : str
360-
The filename to look for.
361-
path_dirs : str, None or sequence of str
362-
The sequence of paths to look for the file in. If None, the filename
363-
need to be absolute or be in the cwd. If a string, the string is
364-
put into a sequence and the searched. If a sequence, walk through
365-
each element and join with ``filename``, calling :func:`expandvars`
366-
and :func:`expanduser` before testing for existence.
358+
The filename to look for. Must be a relative path.
359+
path_dirs : sequence of str
360+
The sequence of paths to look in for the file.
361+
Walk through each element and join with ``filename``.
362+
Only after ensuring the path resolves within the directory is it checked for existence.
367363
368364
Returns
369365
-------
370-
Raises :exc:`IOError` or returns absolute path to file.
366+
Raises :exc:`OSError` or returns absolute path to file.
371367
"""
372-
373-
# If paths are quoted, abspath gets confused, strip them...
374-
filename = filename.strip('"').strip("'")
375-
# If the input is an absolute path, just check it exists
376-
if os.path.isabs(filename) and os.path.isfile(filename):
377-
return filename
378-
379-
if path_dirs is None:
380-
path_dirs = ("",)
381-
elif isinstance(path_dirs, str):
382-
path_dirs = (path_dirs,)
383-
384-
for path in path_dirs:
385-
if path == ".":
386-
path = os.getcwd() # noqa: PLW2901
387-
testname = expand_path(os.path.join(path, filename))
388-
if os.path.isfile(testname):
389-
return os.path.abspath(testname)
368+
file_path = Path(filename)
369+
370+
# If the input is an absolute path, reject it
371+
if file_path.is_absolute():
372+
msg = f"{filename} is absolute, filefind only accepts relative paths."
373+
raise OSError(msg)
374+
375+
for path_str in path_dirs:
376+
path = Path(path_str).absolute()
377+
test_path = path / file_path
378+
# os.path.abspath resolves '..', but Path.absolute() doesn't
379+
# Path.resolve() does, but traverses symlinks, which we don't want
380+
test_path = Path(os.path.abspath(test_path))
381+
if sys.version_info >= (3, 9):
382+
if not test_path.is_relative_to(path):
383+
# points outside root, e.g. via `filename='../foo'`
384+
continue
385+
else:
386+
# is_relative_to is new in 3.9
387+
try:
388+
test_path.relative_to(path)
389+
except ValueError:
390+
# points outside root, e.g. via `filename='../foo'`
391+
continue
392+
# make sure we don't call is_file before we know it's a file within a prefix
393+
# GHSA-hrw6-wg82-cm62 - can leak password hash on windows.
394+
if test_path.is_file():
395+
return os.path.abspath(test_path)
390396

391397
msg = f"File {filename!r} does not exist in any of the search paths: {path_dirs!r}"
392398
raise OSError(msg)
393399

394400

395-
def expand_path(s: str) -> str:
396-
"""Expand $VARS and ~names in a string, like a shell
397-
398-
:Examples:
399-
In [2]: os.environ['FOO']='test'
400-
In [3]: expand_path('variable FOO is $FOO')
401-
Out[3]: 'variable FOO is test'
402-
"""
403-
# This is a pretty subtle hack. When expand user is given a UNC path
404-
# on Windows (\\server\share$\%username%), os.path.expandvars, removes
405-
# the $ to get (\\server\share\%username%). I think it considered $
406-
# alone an empty var. But, we need the $ to remains there (it indicates
407-
# a hidden share).
408-
if os.name == "nt":
409-
s = s.replace("$\\", "IPYTHON_TEMP")
410-
s = os.path.expandvars(os.path.expanduser(s))
411-
if os.name == "nt":
412-
s = s.replace("IPYTHON_TEMP", "$\\")
413-
return s
414-
415-
416401
def import_item(name: str) -> Any:
417402
"""Import and return ``bar`` given the string ``foo.bar``.
418403
Calling ``bar = import_item("foo.bar")`` is the functional equivalent of

tests/test_utils.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from jupyter_server.utils import (
1414
check_pid,
1515
check_version,
16+
filefind,
1617
is_namespace_package,
1718
path2url,
1819
run_sync_in_loop,
@@ -125,3 +126,41 @@ def test_unix_socket_in_use(tmp_path):
125126
sock.listen(0)
126127
assert unix_socket_in_use(server_address)
127128
sock.close()
129+
130+
131+
@pytest.mark.parametrize(
132+
"filename, result",
133+
[
134+
("/foo", OSError),
135+
("../c/in-c", OSError),
136+
("in-a", "a/in-a"),
137+
("in-b", "b/in-b"),
138+
("in-both", "a/in-both"),
139+
(r"\in-a", OSError),
140+
("not-found", OSError),
141+
],
142+
)
143+
def test_filefind(tmp_path, filename, result):
144+
a = tmp_path / "a"
145+
a.mkdir()
146+
b = tmp_path / "b"
147+
b.mkdir()
148+
c = tmp_path / "c"
149+
c.mkdir()
150+
for parent in (a, b):
151+
with parent.joinpath("in-both").open("w"):
152+
pass
153+
with a.joinpath("in-a").open("w"):
154+
pass
155+
with b.joinpath("in-b").open("w"):
156+
pass
157+
with c.joinpath("in-c").open("w"):
158+
pass
159+
160+
if isinstance(result, str):
161+
found = filefind(filename, [str(a), str(b)])
162+
found_relative = Path(found).relative_to(tmp_path)
163+
assert str(found_relative) == result
164+
else:
165+
with pytest.raises(result):
166+
filefind(filename, [str(a), str(b)])

0 commit comments

Comments
 (0)