From 8041d6f96ad374cedac6eec79d9b3be157c0795c Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 2 Mar 2026 14:44:04 +0200 Subject: [PATCH 1/9] Replace os.path.commonprefix() call with commonpath() (#280) --- src/manage/fsutils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/manage/fsutils.py b/src/manage/fsutils.py index 3fa29c3..50aaab2 100644 --- a/src/manage/fsutils.py +++ b/src/manage/fsutils.py @@ -153,7 +153,10 @@ def rmtree(path, after_5s_warning=None, remove_ext_first=()): _rmdir(d, on_fail=to_warn.append, on_isfile=to_unlink) if to_warn: - f = os.path.commonprefix(to_warn) + try: + f = os.path.commonpath(to_warn) + except ValueError: + f = None if f: LOGGER.warn("Failed to remove %s", f) else: From 2bdea91220d655a56423469f2d10c7aecd2ef6dc Mon Sep 17 00:00:00 2001 From: Jesse205 <51242302+Jesse205@users.noreply.github.com> Date: Fri, 20 Mar 2026 18:54:25 +0800 Subject: [PATCH 2/9] Fix shebang auto-upgrade to windowed mode for fallback python.exe commands (#286) --- src/manage/scriptutils.py | 2 +- tests/conftest.py | 8 +++++--- tests/test_install_command.py | 2 +- tests/test_scriptutils.py | 19 +++++++++++++++++++ 4 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/manage/scriptutils.py b/src/manage/scriptutils.py index 4dda417..784ab01 100644 --- a/src/manage/scriptutils.py +++ b/src/manage/scriptutils.py @@ -90,7 +90,7 @@ def _find_shebang_command(cmd, full_cmd, *, windowed=None): return cmd.get_install_to_run(f"PythonCore/{tag}", windowed=True) if sh_cmd.match("python*.exe"): tag = sh_cmd.name[6:-4] - return cmd.get_install_to_run(f"PythonCore/{tag}") + return cmd.get_install_to_run(f"PythonCore/{tag}", windowed=windowed) raise LookupError diff --git a/tests/conftest.py b/tests/conftest.py index c4c59a3..9e79973 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -158,8 +158,10 @@ def __init__(self, global_dir, installs=[]): self.shebang_can_run_anything_silently = False self.scratch = {} - def get_installs(self, *, include_unmanaged=True, set_default=True): - return self.installs + def get_installs(self, *, include_unmanaged=False, set_default=True): + if include_unmanaged: + return self.installs + return [i for i in self.installs if not i.get("unmanaged", 0)] def get_install_to_run(self, tag, *, windowed=False): if windowed: @@ -171,7 +173,7 @@ def get_install_to_run(self, tag, *, windowed=False): company, _, tag = tag.replace("/", "\\").rpartition("\\") return [i for i in self.installs - if i["tag"] == tag and (not company or i["company"] == company)][0] + if (not tag or i["tag"] == tag) and (not company or i["company"] == company)][0] @pytest.fixture diff --git a/tests/test_install_command.py b/tests/test_install_command.py index 9c76ae1..feda93d 100644 --- a/tests/test_install_command.py +++ b/tests/test_install_command.py @@ -288,7 +288,7 @@ def get_log_file(self): def get_installs(self): return self.installs - def get_install_to_run(self, tag): + def get_install_to_run(self, tag=None, script=None, *, windowed=False): for i in self.installs: if i["tag"] == tag or f"{i['company']}/{i['tag']}" == tag: return i diff --git a/tests/test_scriptutils.py b/tests/test_scriptutils.py index 6165053..f81d575 100644 --- a/tests/test_scriptutils.py +++ b/tests/test_scriptutils.py @@ -157,6 +157,25 @@ def t(n): assert t("pythonw1.0")["executable"].match("pythonw.exe") +def test_unmanaged_py_shebang(fake_config, tmp_path): + inst = _fake_install("1.0", company="PythonCore", prefix=PurePath("C:\\TestRoot")) + inst["unmanaged"] = 1 + inst["run-for"] = [ + dict(name="python.exe", target=".\\python.exe"), + dict(name="pythonw.exe", target=".\\pythonw.exe", windowed=1), + ] + fake_config.installs[:] = [inst] + + def t(n): + return _find_shebang_command(fake_config, n, windowed=False) + + # Finds the install's default executable + assert t("python")["executable"].match("test-binary-1.0.exe") + assert t("python1.0")["executable"].match("test-binary-1.0.exe") + # Finds the install's run-for executable with windowed=1 + assert t("pythonw")["executable"].match("pythonw.exe") + assert t("pythonw1.0")["executable"].match("pythonw.exe") + @pytest.mark.parametrize("script, expect", [ ("# not a coding comment", None), From 452d52744f654f4d20829211240e07ad8b197ecf Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Thu, 26 Mar 2026 16:31:35 +0000 Subject: [PATCH 3/9] Handle some error cases during uninstall purge. (#289) Fixes #288 --- src/manage/pep514utils.py | 10 +++++++--- src/manage/uninstall_command.py | 3 ++- tests/conftest.py | 13 +++++++++---- tests/test_uninstall_command.py | 8 ++++++++ 4 files changed, 26 insertions(+), 8 deletions(-) diff --git a/src/manage/pep514utils.py b/src/manage/pep514utils.py index 91ccb72..1976653 100644 --- a/src/manage/pep514utils.py +++ b/src/manage/pep514utils.py @@ -224,18 +224,20 @@ def _is_tag_managed(company_key, tag_name, *, creating=False, allow_warn=True): def _split_root(root_name): if not root_name: LOGGER.verbose("Skipping registry shortcuts as PEP 514 registry root is not set.") - return + return None, None hive_name, _, name = root_name.partition("\\") try: hive = getattr(winreg, hive_name.upper()) except AttributeError: - LOGGER.verbose("Skipping registry shortcuts as %s\\%s is not a valid key", root_name) - return + LOGGER.verbose("Skipping registry shortcuts as %s\\%s is not a valid key", root_name, hive_name) + return None, None return hive, name def update_registry(root_name, install, data, warn_for=[]): hive, name = _split_root(root_name) + if not hive or not name: + return with winreg.CreateKey(hive, name) as root: allow_warn = install_matches_any(install, warn_for) if _is_tag_managed(root, data["Key"], creating=True, allow_warn=allow_warn): @@ -258,6 +260,8 @@ def update_registry(root_name, install, data, warn_for=[]): def cleanup_registry(root_name, keep, warn_for=[]): LOGGER.debug("Cleaning up registry entries") hive, name = _split_root(root_name) + if not hive or not name: + return with _reg_open(hive, name, writable=True) as root: for company_name in list(_iter_keys(root)): any_left = False diff --git a/src/manage/uninstall_command.py b/src/manage/uninstall_command.py index abaebb6..8fefa6c 100644 --- a/src/manage/uninstall_command.py +++ b/src/manage/uninstall_command.py @@ -106,7 +106,8 @@ def execute(cmd): _do_purge_global_dir(cmd.global_dir, warn_msg.format("global commands")) LOGGER.info("Purging all shortcuts") for _, cleanup in SHORTCUT_HANDLERS.values(): - cleanup(cmd, []) + if cleanup: + cleanup(cmd, []) LOGGER.debug("END uninstall_command.execute") return diff --git a/tests/conftest.py b/tests/conftest.py index 9e79973..c4ec097 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -148,10 +148,16 @@ def localserver(): p.wait(5) +REG_TEST_ROOT = r"Software\Python\PyManagerTesting" + + class FakeConfig: def __init__(self, global_dir, installs=[]): - self.root = global_dir.parent if global_dir else None self.global_dir = global_dir + self.root = global_dir.parent if global_dir else None + self.download_dir = self.root / "_cache" if self.root else None + self.start_folder = self.root / "_start" if self.root else None + self.pep514_root = REG_TEST_ROOT self.confirm = False self.installs = list(installs) self.shebang_can_run_anything = True @@ -175,15 +181,14 @@ def get_install_to_run(self, tag, *, windowed=False): return [i for i in self.installs if (not tag or i["tag"] == tag) and (not company or i["company"] == company)][0] + def ask_yn(self, question): + return False if self.confirm else True @pytest.fixture def fake_config(tmp_path): return FakeConfig(tmp_path / "bin") -REG_TEST_ROOT = r"Software\Python\PyManagerTesting" - - class RegistryFixture: def __init__(self, hive, root): self.hive = hive diff --git a/tests/test_uninstall_command.py b/tests/test_uninstall_command.py index 6edb26c..a106d61 100644 --- a/tests/test_uninstall_command.py +++ b/tests/test_uninstall_command.py @@ -17,3 +17,11 @@ def test_purge_global_dir(monkeypatch, registry, tmp_path): assert registry.getvalueandkind("", "Path") == ( rf"C:\A;{tmp_path}\X;C:\B;%PTH%;C:\%D%\E", winreg.REG_SZ) assert not list(tmp_path.iterdir()) + + +def test_null_purge(fake_config): + cmd = fake_config + cmd.args = ["--purge"] + cmd.confirm = False + cmd.purge = True + UC.execute(cmd) From 4a8e8b2b3191305b6a4270e71e8553027be20a57 Mon Sep 17 00:00:00 2001 From: LAKSHMIKANTHAN K Date: Fri, 27 Mar 2026 03:31:42 +0530 Subject: [PATCH 4/9] Use environment variables instead of string interpolation in PowerShell downloader (#291) --- src/manage/urlutils.py | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/src/manage/urlutils.py b/src/manage/urlutils.py index 71a8f5f..e8a3808 100644 --- a/src/manage/urlutils.py +++ b/src/manage/urlutils.py @@ -277,6 +277,7 @@ def _powershell_urlopen(request): def _powershell_urlretrieve(request): from base64 import b64encode + import json import subprocess headers = request.headers @@ -287,23 +288,27 @@ def _powershell_urlretrieve(request): if auth: headers = {**headers, "Authorization": _basic_auth_header(*auth)} - def _f(v): - if isinstance(v, str): - return "'" + v.replace("'", "''") + "'" - return str(v) - - ps_headers = " ".join(f"{k!r}={_f(v)};" for k, v in headers.items()) - powershell = Path(os.getenv("SystemRoot")) / "System32/WindowsPowerShell/v1.0/powershell.exe" - script = fr"""$ProgressPreference = "SilentlyContinue" -$headers = @{{ {ps_headers} }} -$r = Invoke-WebRequest '{request.url}' -UseBasicParsing ` + # Security hardening: avoid PowerShell command injection by using env vars instead of interpolation + script = r"""$ProgressPreference = "SilentlyContinue" +$url = $env:PYMANAGER_URL +$outfile = $env:PYMANAGER_OUTFILE +$method = $env:PYMANAGER_METHOD +$headers = ConvertFrom-Json $env:PYMANAGER_HEADERS +$r = Invoke-WebRequest -Uri $url -UseBasicParsing ` -Headers $headers ` -UseDefaultCredentials ` - -Method "{request.method}" ` - -OutFile "{request.outfile}" + -Method $method ` + -OutFile $outfile """ - LOGGER.debug("PowerShell script: %s", script) + LOGGER.debug("PowerShell download invoked (env-based)") + env = os.environ.copy() + env.update({ + "PYMANAGER_URL": request.url, + "PYMANAGER_OUTFILE": str(request.outfile), + "PYMANAGER_METHOD": request.method, + "PYMANAGER_HEADERS": json.dumps(headers), + }) with subprocess.Popen( [powershell, "-ExecutionPolicy", "Bypass", @@ -312,6 +317,7 @@ def _f(v): "-EncodedCommand", b64encode(script.encode("utf-16-le")) ], cwd=request.outfile.parent, + env=env, creationflags=subprocess.CREATE_NO_WINDOW, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, From 23b3e12b5ff62b5871e31377420acb28b6bdfeed Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Tue, 31 Mar 2026 20:50:48 +0100 Subject: [PATCH 5/9] Fix Invoke-WebRequest IDictionary cast error. (#297) Originally fixed in #292 Co-authored-by: badassletchu@gmail.com --- src/manage/urlutils.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/manage/urlutils.py b/src/manage/urlutils.py index e8a3808..1f0005e 100644 --- a/src/manage/urlutils.py +++ b/src/manage/urlutils.py @@ -294,7 +294,15 @@ def _powershell_urlretrieve(request): $url = $env:PYMANAGER_URL $outfile = $env:PYMANAGER_OUTFILE $method = $env:PYMANAGER_METHOD -$headers = ConvertFrom-Json $env:PYMANAGER_HEADERS +$headersObj = ConvertFrom-Json $env:PYMANAGER_HEADERS +$headers = @{} +if ($headersObj -ne $null) { + $headersObj.PSObject.Properties | ForEach-Object { + $name = $_.Name + $value = $_.Value + $headers[$name] = if ($value -eq $null) { "" } else { $value.ToString() } + } +} $r = Invoke-WebRequest -Uri $url -UseBasicParsing ` -Headers $headers ` -UseDefaultCredentials ` From 3755cde21d33a34f8570c09aa173a7d98e837164 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Tue, 31 Mar 2026 21:04:50 +0100 Subject: [PATCH 6/9] Add additional help when execution aliases are not ready. (#298) Fixes #294 --- src/manage/firstrun.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/manage/firstrun.py b/src/manage/firstrun.py index f19127d..f17ce96 100644 --- a/src/manage/firstrun.py +++ b/src/manage/firstrun.py @@ -348,6 +348,8 @@ def first_run(cmd): "execution aliases!W!' settings page and enabling each " "item labelled '!B!Python (default)!W!' and '!B!Python " "install manager!W!'.\n", wrap=True) + LOGGER.print("If the items are already enabled, you may need to disable " + "and re-enable them.\n", wrap=True) if ( cmd.confirm and not cmd.ask_ny("Open Settings now, so you can modify !B!App " From 4f109648f19bec93abfb6ea5fbb3f7d305fa0518 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Tue, 31 Mar 2026 21:05:08 +0100 Subject: [PATCH 7/9] Allow python3 shebangs to use default runtime. (#296) Fixes #249 --- src/manage/scriptutils.py | 12 ++++++++++-- tests/conftest.py | 11 +++++++++-- tests/test_scriptutils.py | 12 +++++++----- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/src/manage/scriptutils.py b/src/manage/scriptutils.py index 784ab01..d203d8c 100644 --- a/src/manage/scriptutils.py +++ b/src/manage/scriptutils.py @@ -48,8 +48,16 @@ def _find_shebang_command(cmd, full_cmd, *, windowed=None): if not sh_cmd.match("*.exe"): sh_cmd = sh_cmd.with_name(sh_cmd.name + ".exe") - is_wdefault = sh_cmd.match("pythonw.exe") or sh_cmd.match("pyw.exe") - is_default = is_wdefault or sh_cmd.match("python.exe") or sh_cmd.match("py.exe") + is_wdefault = ( + sh_cmd.match("pythonw.exe") + or sh_cmd.match("pyw.exe") + or sh_cmd.match("pythonw3.exe") + ) + is_default = is_wdefault or ( + sh_cmd.match("python.exe") + or sh_cmd.match("py.exe") + or sh_cmd.match("python3.exe") + ) # Internal logic error, but non-fatal, if it has no value assert windowed is not None diff --git a/tests/conftest.py b/tests/conftest.py index c4ec097..a08d35f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -178,8 +178,15 @@ def get_install_to_run(self, tag, *, windowed=False): return i company, _, tag = tag.replace("/", "\\").rpartition("\\") - return [i for i in self.installs - if (not tag or i["tag"] == tag) and (not company or i["company"] == company)][0] + try: + found = [i for i in self.installs + if (not tag or i["tag"] == tag) and (not company or i["company"] == company)] + except LookupError as ex: + # LookupError is expected from this function, so make sure we don't raise it here + raise RuntimeError from ex + if found: + return found[0] + raise LookupError(tag) def ask_yn(self, question): return False if self.confirm else True diff --git a/tests/test_scriptutils.py b/tests/test_scriptutils.py index f81d575..9ccf506 100644 --- a/tests/test_scriptutils.py +++ b/tests/test_scriptutils.py @@ -139,8 +139,8 @@ def test_read_shebang_windowed(fake_config, tmp_path, script, expect, windowed): def test_default_py_shebang(fake_config, tmp_path): inst = _fake_install("1.0", company="PythonCore", prefix=PurePath("C:\\TestRoot"), default=True) inst["run-for"] = [ - dict(name="python.exe", target=".\\python.exe"), - dict(name="pythonw.exe", target=".\\pythonw.exe", windowed=1), + dict(name="othername.exe", target=".\\test-binary-1.0.exe"), + dict(name="othernamew.exe", target=".\\test-binary-1.0-w.exe", windowed=1), ] fake_config.installs[:] = [inst] @@ -150,11 +150,13 @@ def t(n): # Finds the install's default executable assert t("python")["executable"].match("test-binary-1.0.exe") assert t("py")["executable"].match("test-binary-1.0.exe") + assert t("python3")["executable"].match("test-binary-1.0.exe") assert t("python1.0")["executable"].match("test-binary-1.0.exe") # Finds the install's run-for executable with windowed=1 - assert t("pythonw")["executable"].match("pythonw.exe") - assert t("pyw")["executable"].match("pythonw.exe") - assert t("pythonw1.0")["executable"].match("pythonw.exe") + assert t("pythonw")["executable"].match("test-binary-1.0-w.exe") + assert t("pyw")["executable"].match("test-binary-1.0-w.exe") + assert t("pythonw3")["executable"].match("test-binary-1.0-w.exe") + assert t("pythonw1.0")["executable"].match("test-binary-1.0-w.exe") def test_unmanaged_py_shebang(fake_config, tmp_path): From 52e9950a13209b339496917a194c54cd805c66d0 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Tue, 31 Mar 2026 22:59:49 +0100 Subject: [PATCH 8/9] Avoid crashing when invalid __install__.json file exists. (#299) Fixes #293 --- .github/workflows/build.yml | 14 ++++++++++++++ ci/release.yml | 15 +++++++++++++++ src/manage/installs.py | 8 ++++++++ src/manage/uninstall_command.py | 4 +--- tests/conftest.py | 12 ++++++------ tests/test_uninstall_command.py | 2 +- 6 files changed, 45 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 94671db..6309a5f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -182,6 +182,20 @@ jobs: PYMANAGER_DEBUG: true shell: powershell + - name: 'Test purge' + run: | + $env:PYTHON_MANAGER_CONFIG = (gi $env:PYTHON_MANAGER_CONFIG).FullName + pymanager uninstall --purge -y + if (Test-Path test_installs) { + dir -r test_installs + } else { + Write-Host "test_installs directory has been deleted" + } + env: + PYTHON_MANAGER_INCLUDE_UNMANAGED: false + PYTHON_MANAGER_CONFIG: .\test-config.json + PYMANAGER_DEBUG: true + - name: 'Offline bundle download and install' run: | pymanager list --online 3 3-32 3-64 3-arm64 diff --git a/ci/release.yml b/ci/release.yml index 01336fe..d2fcbcd 100644 --- a/ci/release.yml +++ b/ci/release.yml @@ -323,6 +323,21 @@ stages: PYTHON_MANAGER_CONFIG: .\test-config.json PYMANAGER_DEBUG: true + - powershell: | + $env:PYTHON_MANAGER_CONFIG = (gi $env:PYTHON_MANAGER_CONFIG).FullName + pymanager uninstall --purge -y + if (Test-Path test_installs) { + dir -r test_installs + } else { + Write-Host "test_installs directory has been deleted" + } + displayName: 'Test purge' + timeoutInMinutes: 5 + env: + PYTHON_MANAGER_INCLUDE_UNMANAGED: false + PYTHON_MANAGER_CONFIG: .\test-config.json + PYMANAGER_DEBUG: true + - powershell: | pymanager list --online 3 3-32 3-64 3-arm64 pymanager install --download .\bundle 3 3-32 3-64 3-arm64 diff --git a/src/manage/installs.py b/src/manage/installs.py index 4b1dfa5..f8150cd 100644 --- a/src/manage/installs.py +++ b/src/manage/installs.py @@ -23,6 +23,14 @@ def _get_installs(install_dir): try: with p.open() as f: j = json.load(f) + except ValueError: + LOGGER.warn( + "Failed to read install at %s. You may have a broken " + "install, which can be cleaned up by deleting the directory.", + d + ) + LOGGER.debug("ERROR", exc_info=True) + continue except FileNotFoundError: continue diff --git a/src/manage/uninstall_command.py b/src/manage/uninstall_command.py index 8fefa6c..28735fd 100644 --- a/src/manage/uninstall_command.py +++ b/src/manage/uninstall_command.py @@ -66,9 +66,7 @@ def _do_purge_global_dir(global_dir, warn_msg, *, hive=None, subkey="Environment if not global_dir.is_dir(): return LOGGER.info("Purging global commands from %s", global_dir) - for f in _iterdir(global_dir): - LOGGER.debug("Purging %s", f) - rmtree(f, after_5s_warning=warn_msg) + rmtree(global_dir, after_5s_warning=warn_msg) def execute(cmd): diff --git a/tests/conftest.py b/tests/conftest.py index a08d35f..32e5629 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -152,11 +152,11 @@ def localserver(): class FakeConfig: - def __init__(self, global_dir, installs=[]): - self.global_dir = global_dir - self.root = global_dir.parent if global_dir else None - self.download_dir = self.root / "_cache" if self.root else None - self.start_folder = self.root / "_start" if self.root else None + def __init__(self, root, installs=[]): + self.root = self.install_dir = root + self.global_dir = root / "bin" if root else None + self.download_dir = root / "_cache" if root else None + self.start_folder = root / "_start" if root else None self.pep514_root = REG_TEST_ROOT self.confirm = False self.installs = list(installs) @@ -193,7 +193,7 @@ def ask_yn(self, question): @pytest.fixture def fake_config(tmp_path): - return FakeConfig(tmp_path / "bin") + return FakeConfig(tmp_path) class RegistryFixture: diff --git a/tests/test_uninstall_command.py b/tests/test_uninstall_command.py index a106d61..050a808 100644 --- a/tests/test_uninstall_command.py +++ b/tests/test_uninstall_command.py @@ -16,7 +16,7 @@ def test_purge_global_dir(monkeypatch, registry, tmp_path): UC._do_purge_global_dir(tmp_path, "SLOW WARNING", hive=registry.hive, subkey=registry.root) assert registry.getvalueandkind("", "Path") == ( rf"C:\A;{tmp_path}\X;C:\B;%PTH%;C:\%D%\E", winreg.REG_SZ) - assert not list(tmp_path.iterdir()) + assert not tmp_path.is_dir() or not list(tmp_path.iterdir()) def test_null_purge(fake_config): From 75c7c3531d74d1835088508ec02eafa90aa7a0f6 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Tue, 31 Mar 2026 23:24:22 +0100 Subject: [PATCH 9/9] Simplifies alias search path sanitisation. (#301) Fixes CVE-2026-5271 --- src/manage/aliasutils.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/manage/aliasutils.py b/src/manage/aliasutils.py index 76c4701..628d78e 100644 --- a/src/manage/aliasutils.py +++ b/src/manage/aliasutils.py @@ -10,19 +10,20 @@ DEFAULT_SITE_DIRS = ["Lib\\site-packages", "Scripts"] +# Our script removes sys.path[0] if empty to avoid trivial search path hijacks. +# In virtually all cases it should be the directory where our scripts are +# generated, which has no importable packages (unless there are unauthorised +# modifications, which are out of scope for our security threat model). +# We don't try to be any more clever, since we don't know what kind of +# interpreter we are running inside - this script may be generated for any +# arbitrary executable installed by PyManager, and so it's possible that +# sys.path[0] is already sanitised or entirely unrelated. + SCRIPT_CODE = """import sys -# Clear sys.path[0] if it contains this script. -# Be careful to use the most compatible Python code possible. try: - if sys.path[0]: - if sys.argv[0].startswith(sys.path[0]): - sys.path[0] = "" - else: - open(sys.path[0] + "/" + sys.argv[0], "rb").close() - sys.path[0] = "" -except OSError: - pass + if not sys.path[0]: + del sys.path[0] except AttributeError: pass except IndexError: