Skip to content

Commit

Permalink
gh-57684: Add -P cmdline option and PYTHONSAFEPATH env var (#31542)
Browse files Browse the repository at this point in the history
Add the -P command line option and the PYTHONSAFEPATH environment
variable to not prepend a potentially unsafe path to sys.path.

* Add sys.flags.safe_path flag.
* Add PyConfig.safe_path member.
* Programs/_bootstrap_python.c uses config.safe_path=0.
* Update subprocess._optim_args_from_interpreter_flags() to handle
  the -P command line option.
* Modules/getpath.py sets safe_path to 1 if a "._pth" file is
  present.
  • Loading branch information
vstinner committed May 5, 2022
1 parent f6dd14c commit ada8b6d
Show file tree
Hide file tree
Showing 20 changed files with 174 additions and 35 deletions.
46 changes: 37 additions & 9 deletions Doc/c-api/init_config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,25 @@ PyConfig
See also the :c:member:`~PyConfig.orig_argv` member.
.. c:member:: int safe_path
If equals to zero, ``Py_RunMain()`` prepends a potentially unsafe path to
:data:`sys.path` at startup:
* If :c:member:`argv[0] <PyConfig.argv>` is equal to ``L"-m"``
(``python -m module``), prepend the current working directory.
* If running a script (``python script.py``), prepend the script's
directory. If it's a symbolic link, resolve symbolic links.
* Otherwise (``python -c code`` and ``python``), prepend an empty string,
which means the current working directory.
Set to 1 by the :option:`-P` command line option and the
:envvar:`PYTHONSAFEPATH` environment variable.
Default: ``0`` in Python config, ``1`` in isolated config.
.. versionadded:: 3.11
.. c:member:: wchar_t* base_exec_prefix
:data:`sys.base_exec_prefix`.
Expand Down Expand Up @@ -809,13 +828,14 @@ PyConfig
If greater than 0, enable isolated mode:
* :data:`sys.path` contains neither the script's directory (computed from
``argv[0]`` or the current directory) nor the user's site-packages
directory.
* Set :c:member:`~PyConfig.safe_path` to 1:
don't prepend a potentially unsafe path to :data:`sys.path` at Python
startup.
* Set :c:member:`~PyConfig.use_environment` to 0.
* Set :c:member:`~PyConfig.user_site_directory` to 0: don't add the user
site directory to :data:`sys.path`.
* Python REPL doesn't import :mod:`readline` nor enable default readline
configuration on interactive prompts.
* Set :c:member:`~PyConfig.use_environment` and
:c:member:`~PyConfig.user_site_directory` to 0.
Default: ``0`` in Python mode, ``1`` in isolated mode.
Expand Down Expand Up @@ -1029,12 +1049,13 @@ PyConfig
.. c:member:: wchar_t* run_filename
Filename passed on the command line: trailing command line argument
without :option:`-c` or :option:`-m`.
without :option:`-c` or :option:`-m`. It is used by the
:c:func:`Py_RunMain` function.
For example, it is set to ``script.py`` by the ``python3 script.py arg``
command.
command line.
Used by :c:func:`Py_RunMain`.
See also the :c:member:`PyConfig.skip_source_first_line` option.
Default: ``NULL``.
Expand Down Expand Up @@ -1419,9 +1440,16 @@ site-package directory to :data:`sys.path`.
The following configuration files are used by the path configuration:
* ``pyvenv.cfg``
* ``python._pth`` (Windows only)
* ``._pth`` file (ex: ``python._pth``)
* ``pybuilddir.txt`` (Unix only)
If a ``._pth`` file is present:
* Set :c:member:`~PyConfig.isolated` to 1.
* Set :c:member:`~PyConfig.use_environment` to 0.
* Set :c:member:`~PyConfig.site_import` to 0.
* Set :c:member:`~PyConfig.safe_path` to 1.
The ``__PYVENV_LAUNCHER__`` environment variable is used to set
:c:member:`PyConfig.base_executable`
Expand Down
24 changes: 16 additions & 8 deletions Doc/library/sys.rst
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,7 @@ always available.
:const:`hash_randomization` :option:`-R`
:const:`dev_mode` :option:`-X dev <-X>` (:ref:`Python Development Mode <devmode>`)
:const:`utf8_mode` :option:`-X utf8 <-X>`
:const:`safe_path` :option:`-P`
============================= ================================================================

.. versionchanged:: 3.2
Expand All @@ -539,6 +540,9 @@ always available.
Mode <devmode>` and the ``utf8_mode`` attribute for the new :option:`-X`
``utf8`` flag.

.. versionchanged:: 3.11
Added the ``safe_path`` attribute for :option:`-P` option.


.. data:: float_info

Expand Down Expand Up @@ -1138,15 +1142,19 @@ always available.
the environment variable :envvar:`PYTHONPATH`, plus an installation-dependent
default.

As initialized upon program startup, the first item of this list, ``path[0]``,
is the directory containing the script that was used to invoke the Python
interpreter. If the script directory is not available (e.g. if the interpreter
is invoked interactively or if the script is read from standard input),
``path[0]`` is the empty string, which directs Python to search modules in the
current directory first. Notice that the script directory is inserted *before*
the entries inserted as a result of :envvar:`PYTHONPATH`.
By default, as initialized upon program startup, a potentially unsafe path
is prepended to :data:`sys.path` (*before* the entries inserted as a result
of :envvar:`PYTHONPATH`):

* ``python -m module`` command line: prepend the current working
directory.
* ``python script.py`` command line: prepend the script's directory.
If it's a symbolic link, resolve symbolic links.
* ``python -c code`` and ``python`` (REPL) command lines: prepend an empty
string, which means the current working directory.

The initialization of :data:`sys.path` is documented at :ref:`sys-path-init`.
To not prepend this potentially unsafe path, use the :option:`-P` command
line option or the :envvar:`PYTHONSAFEPATH` environment variable?

A program is free to modify this list for its own purposes. Only strings
and bytes should be added to :data:`sys.path`; all other data types are
Expand Down
31 changes: 30 additions & 1 deletion Doc/using/cmdline.rst
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,8 @@ Miscellaneous options
Ignore all :envvar:`PYTHON*` environment variables, e.g.
:envvar:`PYTHONPATH` and :envvar:`PYTHONHOME`, that might be set.

See also the :option:`-P` and :option:`-I` (isolated) options.


.. cmdoption:: -i

Expand All @@ -271,7 +273,9 @@ Miscellaneous options

.. cmdoption:: -I

Run Python in isolated mode. This also implies -E and -s.
Run Python in isolated mode. This also implies :option:`-E`, :option:`-P`
and :option:`-s` options.

In isolated mode :data:`sys.path` contains neither the script's directory nor
the user's site-packages directory. All :envvar:`PYTHON*` environment
variables are ignored, too. Further restrictions may be imposed to prevent
Expand Down Expand Up @@ -301,6 +305,23 @@ Miscellaneous options
Modify ``.pyc`` filenames according to :pep:`488`.


.. cmdoption:: -P

Don't prepend a potentially unsafe path to :data:`sys.path`:

* ``python -m module`` command line: Don't prepend the current working
directory.
* ``python script.py`` command line: Don't prepend the script's directory.
If it's a symbolic link, resolve symbolic links.
* ``python -c code`` and ``python`` (REPL) command lines: Don't prepend an
empty string, which means the current working directory.

See also the :envvar:`PYTHONSAFEPATH` environment variable, and :option:`-E`
and :option:`-I` (isolated) options.

.. versionadded:: 3.11


.. cmdoption:: -q

Don't display the copyright and version messages even in interactive mode.
Expand Down Expand Up @@ -583,6 +604,14 @@ conflict.
within a Python program as the variable :data:`sys.path`.


.. envvar:: PYTHONSAFEPATH

If this is set to a non-empty string, don't prepend a potentially unsafe
path to :data:`sys.path`: see the :option:`-P` option for details.

.. versionadded:: 3.11


.. envvar:: PYTHONPLATLIBDIR

If this is set to a non-empty string, it overrides the :data:`sys.platlibdir`
Expand Down
10 changes: 10 additions & 0 deletions Doc/whatsnew/3.11.rst
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,11 @@ Other Language Changes
pickles instance attributes implemented as :term:`slots <__slots__>`.
(Contributed by Serhiy Storchaka in :issue:`26579`.)

* Add :option:`-P` command line option and :envvar:`PYTHONSAFEPATH` environment
variable to not prepend a potentially unsafe path to :data:`sys.path` such as
the current directory, the script's directory or an empty string.
(Contributed by Victor Stinner in :gh:`57684`.)


Other CPython Implementation Changes
====================================
Expand Down Expand Up @@ -636,6 +641,9 @@ sys
(equivalent to ``sys.exc_info()[1]``).
(Contributed by Irit Katriel in :issue:`46328`.)

* Add the :data:`sys.flags.safe_path <sys.flags>` flag.
(Contributed by Victor Stinner in :gh:`57684`.)


sysconfig
---------
Expand Down Expand Up @@ -1480,6 +1488,8 @@ New Features
representation of exceptions.
(Contributed by Irit Katriel in :issue:`46343`.)

* Added the :c:member:`PyConfig.safe_path` member.
(Contributed by Victor Stinner in :gh:`57684`.)

Porting to Python 3.11
----------------------
Expand Down
1 change: 1 addition & 0 deletions Include/cpython/initconfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ typedef struct PyConfig {
#endif
wchar_t *check_hash_pycs_mode;
int use_frozen_modules;
int safe_path;

/* --- Path configuration inputs ------------ */
int pathconfig_warnings;
Expand Down
6 changes: 4 additions & 2 deletions Lib/subprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -313,12 +313,14 @@ def _args_from_interpreter_flags():
args.append('-E')
if sys.flags.no_user_site:
args.append('-s')
if sys.flags.safe_path:
args.append('-P')

# -W options
warnopts = sys.warnoptions[:]
bytes_warning = sys.flags.bytes_warning
xoptions = getattr(sys, '_xoptions', {})
dev_mode = ('dev' in xoptions)
bytes_warning = sys.flags.bytes_warning
dev_mode = sys.flags.dev_mode

if bytes_warning > 1:
warnopts.remove("error::BytesWarning")
Expand Down
10 changes: 6 additions & 4 deletions Lib/test/test_cmd_line.py
Original file line number Diff line number Diff line change
Expand Up @@ -579,13 +579,13 @@ def test_unknown_options(self):
'Cannot run -I tests when PYTHON env vars are required.')
def test_isolatedmode(self):
self.verify_valid_flag('-I')
self.verify_valid_flag('-IEs')
self.verify_valid_flag('-IEPs')
rc, out, err = assert_python_ok('-I', '-c',
'from sys import flags as f; '
'print(f.no_user_site, f.ignore_environment, f.isolated)',
'print(f.no_user_site, f.ignore_environment, f.isolated, f.safe_path)',
# dummyvar to prevent extraneous -E
dummyvar="")
self.assertEqual(out.strip(), b'1 1 1')
self.assertEqual(out.strip(), b'1 1 1 True')
with os_helper.temp_cwd() as tmpdir:
fake = os.path.join(tmpdir, "uuid.py")
main = os.path.join(tmpdir, "main.py")
Expand Down Expand Up @@ -880,14 +880,16 @@ def test_sys_flags_not_set(self):
# Issue 31845: a startup refactoring broke reading flags from env vars
expected_outcome = """
(sys.flags.debug == sys.flags.optimize ==
sys.flags.dont_write_bytecode == sys.flags.verbose == 0)
sys.flags.dont_write_bytecode ==
sys.flags.verbose == sys.flags.safe_path == 0)
"""
self.run_ignoring_vars(
expected_outcome,
PYTHONDEBUG="1",
PYTHONOPTIMIZE="1",
PYTHONDONTWRITEBYTECODE="1",
PYTHONVERBOSE="1",
PYTHONSAFEPATH="1",
)

class SyntaxErrorTests(unittest.TestCase):
Expand Down
13 changes: 11 additions & 2 deletions Lib/test/test_embed.py
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,7 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase):
'_init_main': 1,
'_isolated_interpreter': 0,
'use_frozen_modules': not Py_DEBUG,
'safe_path': 0,
'_is_python_build': IGNORE_CONFIG,
}
if MS_WINDOWS:
Expand All @@ -496,6 +497,7 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase):
isolated=1,
use_environment=0,
user_site_directory=0,
safe_path=1,
dev_mode=0,
install_signal_handlers=0,
use_hash_seed=0,
Expand Down Expand Up @@ -855,6 +857,7 @@ def test_init_from_config(self):
'faulthandler': 1,
'platlibdir': 'my_platlibdir',
'module_search_paths': self.IGNORE_CONFIG,
'safe_path': 1,

'check_hash_pycs_mode': 'always',
'pathconfig_warnings': 0,
Expand Down Expand Up @@ -889,6 +892,7 @@ def test_init_compat_env(self):
'warnoptions': ['EnvVar'],
'platlibdir': 'env_platlibdir',
'module_search_paths': self.IGNORE_CONFIG,
'safe_path': 1,
}
self.check_all_configs("test_init_compat_env", config, preconfig,
api=API_COMPAT)
Expand Down Expand Up @@ -919,6 +923,7 @@ def test_init_python_env(self):
'warnoptions': ['EnvVar'],
'platlibdir': 'env_platlibdir',
'module_search_paths': self.IGNORE_CONFIG,
'safe_path': 1,
}
self.check_all_configs("test_init_python_env", config, preconfig,
api=API_PYTHON)
Expand Down Expand Up @@ -959,12 +964,13 @@ def test_preinit_parse_argv(self):
}
config = {
'argv': ['script.py'],
'orig_argv': ['python3', '-X', 'dev', 'script.py'],
'orig_argv': ['python3', '-X', 'dev', '-P', 'script.py'],
'run_filename': os.path.abspath('script.py'),
'dev_mode': 1,
'faulthandler': 1,
'warnoptions': ['default'],
'xoptions': ['dev'],
'safe_path': 1,
}
self.check_all_configs("test_preinit_parse_argv", config, preconfig,
api=API_PYTHON)
Expand All @@ -975,7 +981,7 @@ def test_preinit_dont_parse_argv(self):
'isolated': 0,
}
argv = ["python3",
"-E", "-I",
"-E", "-I", "-P",
"-X", "dev",
"-X", "utf8",
"script.py"]
Expand All @@ -990,6 +996,7 @@ def test_preinit_dont_parse_argv(self):
def test_init_isolated_flag(self):
config = {
'isolated': 1,
'safe_path': 1,
'use_environment': 0,
'user_site_directory': 0,
}
Expand All @@ -999,6 +1006,7 @@ def test_preinit_isolated1(self):
# _PyPreConfig.isolated=1, _PyCoreConfig.isolated not set
config = {
'isolated': 1,
'safe_path': 1,
'use_environment': 0,
'user_site_directory': 0,
}
Expand All @@ -1008,6 +1016,7 @@ def test_preinit_isolated2(self):
# _PyPreConfig.isolated=0, _PyCoreConfig.isolated=1
config = {
'isolated': 1,
'safe_path': 1,
'use_environment': 0,
'user_site_directory': 0,
}
Expand Down
4 changes: 3 additions & 1 deletion Lib/test/test_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,7 @@ def test_args_from_interpreter_flags(self):
['-E'],
['-v'],
['-b'],
['-P'],
['-q'],
['-I'],
# same option multiple times
Expand All @@ -541,7 +542,8 @@ def test_args_from_interpreter_flags(self):
with self.subTest(opts=opts):
self.check_options(opts, 'args_from_interpreter_flags')

self.check_options(['-I', '-E', '-s'], 'args_from_interpreter_flags',
self.check_options(['-I', '-E', '-s', '-P'],
'args_from_interpreter_flags',
['-I'])

def test_optim_args_from_interpreter_flags(self):
Expand Down
4 changes: 2 additions & 2 deletions Lib/test/test_sys.py
Original file line number Diff line number Diff line change
Expand Up @@ -669,10 +669,10 @@ def test_sys_flags(self):
"dont_write_bytecode", "no_user_site", "no_site",
"ignore_environment", "verbose", "bytes_warning", "quiet",
"hash_randomization", "isolated", "dev_mode", "utf8_mode",
"warn_default_encoding")
"warn_default_encoding", "safe_path")
for attr in attrs:
self.assertTrue(hasattr(sys.flags, attr), attr)
attr_type = bool if attr == "dev_mode" else int
attr_type = bool if attr in ("dev_mode", "safe_path") else int
self.assertEqual(type(getattr(sys.flags, attr)), attr_type, attr)
self.assertTrue(repr(sys.flags))
self.assertEqual(len(sys.flags), len(attrs))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Add the :option:`-P` command line option and the :envvar:`PYTHONSAFEPATH`
environment variable to not prepend a potentially unsafe path to
:data:`sys.path`. Patch by Victor Stinner.
Loading

0 comments on commit ada8b6d

Please sign in to comment.