From e9bc9013009fd895d3f9aa8c0e6e5b6e86600199 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Fri, 15 May 2026 14:40:32 +0100 Subject: [PATCH 1/4] Fixes risk of installed packages using invalid entrypoint names to overwrite other files --- src/manage/aliasutils.py | 17 ++++++++++++++++- tests/test_alias.py | 12 ++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/manage/aliasutils.py b/src/manage/aliasutils.py index 628d78e..100890f 100644 --- a/src/manage/aliasutils.py +++ b/src/manage/aliasutils.py @@ -3,7 +3,7 @@ from .exceptions import FilesInUseError, NoLauncherTemplateError from .fsutils import atomic_unlink, ensure_tree, unlink from .logging import LOGGER -from .pathutils import Path, relative_to +from .pathutils import Path, PurePath, relative_to from .tagutils import install_matches_any _EXE = ".exe".casefold() @@ -97,6 +97,10 @@ def _create_alias( allow_link=True, _link=os.link): p = cmd.global_dir / name + # Raise exception if someone has tried to get us to write outside of the + # intended directory + if p.relative_to(cmd.global_dir).parts != (name,): + raise ValueError(f"Invalid alias name: {name}") if not p.match("*.exe"): p = p.with_name(p.name + ".exe") if not isinstance(target, Path): @@ -235,6 +239,9 @@ def _parse_entrypoint_line(line): name, sep, rest = line.partition("=") name = name.strip() if name and name[0].isalnum() and sep and rest: + print(PurePath(name).parent) + if PurePath(name).parent: + return None, None, None mod, sep, rest = rest.partition(":") mod = mod.strip() if mod and sep and rest: @@ -382,6 +389,14 @@ def create_aliases(cmd, aliases, *, allow_link=True, _create_alias=_create_alias else: LOGGER.debug("Skipping %s alias because " "the launcher template was not found.", alias.name) + except Exception: + if install_matches_any(alias.install, getattr(cmd, "tags", None)): + LOGGER.warn("Skipping %s alias because an unexpected error " + "occurred.", alias.name) + LOGGER.debug("TRACEBACK", exc_info=True) + else: + LOGGER.debug("Skipping %s alias because an unexpected error " + "occurred.", alias.name, exc_info=True) diff --git a/tests/test_alias.py b/tests/test_alias.py index b1da40d..0039c66 100644 --- a/tests/test_alias.py +++ b/tests/test_alias.py @@ -117,6 +117,16 @@ def test_write_script_alias(alias_checker): alias_checker.check_script(alias_checker.Cmd(), "1.0-32", "testA", windowed=0) +@pytest.mark.parametrize("name", [ + "..\\evil_path", + "dir\\..\\evil_path", + "normal\\subdir", +]) +def test_write_invalid_alias_name(alias_checker, name): + with pytest.raises(ValueError): + alias_checker.check(alias_checker.Cmd(), "1.0-32", name, None) + + def test_write_alias_launcher_missing(fake_config, assert_log, tmp_path): fake_config.launcher_exe = tmp_path / "non-existent.exe" fake_config.default_platform = '-32' @@ -255,6 +265,8 @@ def test_parse_entrypoint_line(): (" name = mod : func ", ("name", "mod", "func")), ("name=mod:func[extra]", ("name", "mod", "func")), ("name=mod:func [extra]", ("name", "mod", "func")), + ("../name=mod:func", (None, None, None)), + ("name/../../../name=mod:func", (None, None, None)), ]: assert expect == AU._parse_entrypoint_line(line) From d488c31e715e795fcc564c83b0edc26bd564a59e Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Mon, 18 May 2026 15:56:44 +0100 Subject: [PATCH 2/4] Fix comparison and remove debug statement --- src/manage/aliasutils.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/manage/aliasutils.py b/src/manage/aliasutils.py index 100890f..42d54fa 100644 --- a/src/manage/aliasutils.py +++ b/src/manage/aliasutils.py @@ -99,7 +99,7 @@ def _create_alias( p = cmd.global_dir / name # Raise exception if someone has tried to get us to write outside of the # intended directory - if p.relative_to(cmd.global_dir).parts != (name,): + if str(p.relative_to(cmd.global_dir)) != name: raise ValueError(f"Invalid alias name: {name}") if not p.match("*.exe"): p = p.with_name(p.name + ".exe") @@ -239,7 +239,6 @@ def _parse_entrypoint_line(line): name, sep, rest = line.partition("=") name = name.strip() if name and name[0].isalnum() and sep and rest: - print(PurePath(name).parent) if PurePath(name).parent: return None, None, None mod, sep, rest = rest.partition(":") From 38f4c5c628f9136f5905c274ab3b0ce10bed8fc1 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Mon, 18 May 2026 16:06:21 +0100 Subject: [PATCH 3/4] Ensure comparison only matches the name --- src/manage/aliasutils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/manage/aliasutils.py b/src/manage/aliasutils.py index 42d54fa..59d3b10 100644 --- a/src/manage/aliasutils.py +++ b/src/manage/aliasutils.py @@ -99,7 +99,7 @@ def _create_alias( p = cmd.global_dir / name # Raise exception if someone has tried to get us to write outside of the # intended directory - if str(p.relative_to(cmd.global_dir)) != name: + if str(p.relative_to(cmd.global_dir)) != PurePath(name).name: raise ValueError(f"Invalid alias name: {name}") if not p.match("*.exe"): p = p.with_name(p.name + ".exe") From 207bea5e008d3f5a76ec3f4697acaa4696423a78 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Mon, 18 May 2026 19:35:31 +0100 Subject: [PATCH 4/4] Review feedback --- src/manage/aliasutils.py | 1 + tests/test_alias.py | 1 + 2 files changed, 2 insertions(+) diff --git a/src/manage/aliasutils.py b/src/manage/aliasutils.py index 59d3b10..ac2b7b4 100644 --- a/src/manage/aliasutils.py +++ b/src/manage/aliasutils.py @@ -239,6 +239,7 @@ def _parse_entrypoint_line(line): name, sep, rest = line.partition("=") name = name.strip() if name and name[0].isalnum() and sep and rest: + # "names" that have a parent directory/slash are invalid if PurePath(name).parent: return None, None, None mod, sep, rest = rest.partition(":") diff --git a/tests/test_alias.py b/tests/test_alias.py index 0039c66..d80c825 100644 --- a/tests/test_alias.py +++ b/tests/test_alias.py @@ -121,6 +121,7 @@ def test_write_script_alias(alias_checker): "..\\evil_path", "dir\\..\\evil_path", "normal\\subdir", + "C:\\absolute\\evil_path", ]) def test_write_invalid_alias_name(alias_checker, name): with pytest.raises(ValueError):