diff --git a/IPython/__init__.py b/IPython/__init__.py index 5d656e4..e12da90 100644 --- a/IPython/__init__.py +++ b/IPython/__init__.py @@ -60,6 +60,10 @@ __author__ = '%s <%s>' % (release.author, release.author_email) __license__ = release.license __version__ = release.version version_info = release.version_info +# list of CVEs that should have been patched in this release. +# this is informational and should not be relied upon. +__patched_cves__ = {"CVE-2022-21699"} + def embed_kernel(module=None, local_ns=None, **kwargs): """Embed and start an IPython kernel in a given scope. diff --git a/IPython/core/application.py b/IPython/core/application.py index e93a106..2b389a6 100644 --- a/IPython/core/application.py +++ b/IPython/core/application.py @@ -157,7 +157,7 @@ class BaseIPythonApplication(Application): config_file_paths = List(Unicode()) @default('config_file_paths') def _config_file_paths_default(self): - return [os.getcwd()] + return [] extra_config_file = Unicode( help="""Path to an extra config file to load. diff --git a/IPython/core/profileapp.py b/IPython/core/profileapp.py index 97434e3..9a1bae5 100644 --- a/IPython/core/profileapp.py +++ b/IPython/core/profileapp.py @@ -181,9 +181,10 @@ class ProfileList(Application): profiles = list_profiles_in(os.getcwd()) if profiles: print() - print("Available profiles in current directory (%s):" % os.getcwd()) - self._print_profiles(profiles) - + print( + "Profiles from CWD have been removed for security reason, see CVE-2022-21699:" + ) + print() print("To use any of the above profiles, start IPython with:") print(" ipython --profile=") diff --git a/IPython/core/profiledir.py b/IPython/core/profiledir.py index 756595a..1e33b55 100644 --- a/IPython/core/profiledir.py +++ b/IPython/core/profiledir.py @@ -188,7 +188,7 @@ class ProfileDir(LoggingConfigurable): is not found, a :class:`ProfileDirError` exception will be raised. The search path algorithm is: - 1. ``os.getcwd()`` + 1. ``os.getcwd()`` # removed for security reason. 2. ``ipython_dir`` Parameters @@ -200,7 +200,7 @@ class ProfileDir(LoggingConfigurable): will be "profile_". """ dirname = u'profile_' + name - paths = [os.getcwd(), ipython_dir] + paths = [ipython_dir] for p in paths: profile_dir = os.path.join(p, dirname) if os.path.isdir(profile_dir): diff --git a/IPython/tests/cve.py b/IPython/tests/cve.py new file mode 100644 index 0000000..026415a --- /dev/null +++ b/IPython/tests/cve.py @@ -0,0 +1,56 @@ +""" +Test that CVEs stay fixed. +""" + +from IPython.utils.tempdir import TemporaryDirectory, TemporaryWorkingDirectory +from pathlib import Path +import random +import sys +import os +import string +import subprocess +import time + +def test_cve_2022_21699(): + """ + Here we test CVE-2022-21699. + + We create a temporary directory, cd into it. + Make a profile file that should not be executed and start IPython in a subprocess, + checking for the value. + + + + """ + + dangerous_profile_dir = Path('profile_default') + + dangerous_startup_dir = dangerous_profile_dir / 'startup' + dangerous_expected = 'CVE-2022-21699-'+''.join([random.choice(string.ascii_letters) for i in range(10)]) + + with TemporaryWorkingDirectory() as t: + dangerous_startup_dir.mkdir(parents=True) + (dangerous_startup_dir/ 'foo.py').write_text(f'print("{dangerous_expected}")') + # 1 sec to make sure FS is flushed. + #time.sleep(1) + cmd = [sys.executable,'-m', 'IPython'] + env = os.environ.copy() + env['IPY_TEST_SIMPLE_PROMPT'] = '1' + + + # First we fake old behavior, making sure the profile is/was actually dangerous + p_dangerous = subprocess.Popen(cmd + [f'--profile-dir={dangerous_profile_dir}'], env=env, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + out_dangerous, err_dangerouns = p_dangerous.communicate(b"exit\r") + assert dangerous_expected in out_dangerous.decode() + + # Now that we know it _would_ have been dangerous, we test it's not loaded + p = subprocess.Popen(cmd, env=env, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + out, err = p.communicate(b"exit\r") + assert b'IPython' in out + assert dangerous_expected not in out.decode() + assert err == b'' + + + diff --git a/docs/source/whatsnew/version8.rst b/docs/source/whatsnew/version8.rst index 640b48a..35454b3 100644 --- a/docs/source/whatsnew/version8.rst +++ b/docs/source/whatsnew/version8.rst @@ -2,6 +2,50 @@ 8.x Series ============ + +IPython 8.0.1 (CVE-2022-21699) +------------------------------ + +IPython 8.0.1, 7.31.1 and 5.11 are security releases that change some default +values in order to prevent potential Execution with Unnecessary Privileges. + +Almost all version of IPython looks for configuration and profiles in current +working directory. Since IPython was developed before pip and environments +existed it was used a convenient way to load code/packages in a project +dependant way. + +In 2022, it is not necessary anymore, and can lead to confusing behavior where +for example cloning a repository and starting IPython or loading a notebook from +any Jupyter-Compatible interface that has ipython set as a kernel can lead to +code execution. + + +I did not find any standard way for packaged to advertise CVEs they fix, I'm +thus trying to add a ``__patched_cves__`` attribute to the IPython module that +list the CVEs that should have been fixed. This attribute is informational only +as if a executable has a flaw, this value can always be changed by an attacker. + +.. code:: + + In [1]: import IPython + + In [2]: IPython.__patched_cves__ + Out[2]: {'CVE-2022-21699'} + + In [3]: 'CVE-2022-21699' in IPython.__patched_cves__ + Out[3]: True + +Thus starting with this version: + + - The current working directory is not searched anymore for profiles or + configurations files. + - Added a ``__patched_cves__`` attribute (set of strings) to IPython module that contain + the list of fixed CVE. This is informational only. + +Further details can be read on the `GitHub Advisory `__ + + + IPython 8.0 -----------