From f37a9f9feb3f6b00cc170113585d55f9869e2c4c Mon Sep 17 00:00:00 2001 From: Dhanu-dynamic Date: Tue, 29 Sep 2026 19:28:51 +0530 Subject: [PATCH] Ignore environment_path in loaded project files (#2110) A .jedi/project.json can be part of a checked out repository, so honouring its environment_path let an untrusted file point Jedi at a binary to execute. Rather than trying to gate that binary (the ownership heuristic in _is_unix_safe_simple is meaningless on Windows, where st_uid is always 0), drop environment_path from loaded project files entirely, the same way load_unsafe_extensions is already ignored. It stays available when a Project is constructed directly by the user. --- jedi/api/project.py | 20 +++++++++----------- test/test_api/test_project.py | 18 ++++-------------- 2 files changed, 13 insertions(+), 25 deletions(-) diff --git a/jedi/api/project.py b/jedi/api/project.py index 66ec35fc..0ff26cab 100644 --- a/jedi/api/project.py +++ b/jedi/api/project.py @@ -63,9 +63,6 @@ class Project: Additionally there are functions to search a whole project. """ _environment = None - # Set for projects loaded from a ``.jedi/project.json``. That file can be - # part of a checked out repository, so it is not trusted to run binaries. - _loaded_from_file = False @staticmethod def _get_config_folder_path(base_path): @@ -90,12 +87,14 @@ class Project: if version == 1: # A code base must not be able to enable the loading of its own - # extensions, see the docs about security. + # extensions or point Jedi at a binary to execute, see the docs + # about security. The project file can be part of a checked out + # repository, so these settings are ignored for loaded projects. if data.pop('load_unsafe_extensions', False): debug.warning('load_unsafe_extensions is ignored for loaded projects') - project = cls(**data) - project._loaded_from_file = True - return project + if data.pop('environment_path', None) is not None: + debug.warning('environment_path is ignored for loaded projects') + return cls(**data) else: raise WrongVersion( "The Jedi version of this project seems newer than what we can handle." @@ -107,7 +106,6 @@ class Project: """ data = dict(self.__dict__) data.pop('_environment', None) - data.pop('_loaded_from_file', None) data.pop('_django', None) # TODO make django setting public? data = {k.lstrip('_'): v for k, v in data.items()} data['path'] = str(data['path']) @@ -252,10 +250,10 @@ class Project: def get_environment(self): if self._environment is None: if self._environment_path is not None: - # An environment path from a loaded project file is checked - # like a scanned virtualenv, see find_virtualenvs. + # environment_path can only be set by the user directly, it is + # never loaded from a project file, so it is trusted. self._environment = create_environment( - self._environment_path, safe=self._loaded_from_file) + self._environment_path, safe=False) else: self._environment = get_cached_default_environment() return self._environment diff --git a/test/test_api/test_project.py b/test/test_api/test_project.py index fd90f150..365fc016 100644 --- a/test/test_api/test_project.py +++ b/test/test_api/test_project.py @@ -7,7 +7,6 @@ import pytest from ..helpers import get_example_dir, set_cwd, root_dir, test_dir from jedi import Interpreter from jedi.api import Project, get_default_project -from jedi.api.environment import InvalidPythonEnvironment from jedi.api.project import _is_potential_project, _CONTAINS_POTENTIAL_PROJECT @@ -56,11 +55,9 @@ def test_load_save_project(tmpdir): assert loaded.added_sys_path == ['/foo'] -def test_load_project_checks_environment_path(tmpdir, monkeypatch): - # The project file can be part of a checked out repository, so the binary - # it points to has to pass the same check as in find_virtualenvs. - monkeypatch.setattr('jedi.api.environment._is_safe', lambda executable_path: False) - +def test_load_project_ignores_environment_path(tmpdir, monkeypatch): + # The project file can be part of a checked out repository, so it must not + # be able to point Jedi at a binary to execute. def _get_subprocess(self): raise RuntimeError('Should not get called!') @@ -68,14 +65,7 @@ def test_load_project_checks_environment_path(tmpdir, monkeypatch): _get_subprocess) Project(tmpdir.strpath, environment_path=sys.executable).save() - with pytest.raises(InvalidPythonEnvironment): - Project.load(tmpdir.strpath).get_environment() - - -def test_load_project_safe_environment_path(tmpdir): - Project(tmpdir.strpath, environment_path=sys.executable).save() - environment = Project.load(tmpdir.strpath).get_environment() - assert environment.executable == sys.executable + assert Project.load(tmpdir.strpath)._environment_path is None def test_load_project_ignores_unsafe_extensions(tmpdir):