-
-
Notifications
You must be signed in to change notification settings - Fork 30.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Avoid adding an empty directory to sys.path when running a module with -m
#77234
Comments
I think this is a really stupid security bug. Running a module with I.e. whereas python3 /usr/lib/module.py wouldn't do that, python3 -mmodule would make it so that following a chdirs in code, imports would be executed from arbitrary locations. Verified on MacOS X, Ubuntu 17.10, using variety of Python versions up to 3.7. |
FWIW, this behavior is documented: https://docs.python.org/3/using/cmdline.html#cmdoption-m With the -c option, at least you could easily remove the sys.path element yourself: python -c 'import sys; sys.path.remove(""); ...' (This works, because sys is always a builtin module, so it won't be imported from cwd.) I don't see any obvious way to make "python -m foo" secure in untrusted cwd, though. python -c 'import sys; sys.path.remove(""); import runpy; runpy._run_module_as_main("foo")' which is quite insane. |
This isn't considered a security issue, as running "python3" interactively behaves in exactly the same way (i.e. tracking changes to the current working directory for the duration of the session), and running "python3 script.py" adds the full path to the current directory. In all cases, the expectation is that end users will at least enable isolated mode if they don't want to risk importing arbitrary code from user controlled directories. $ echo "print('Hello')" > foo.py
$ python3 -m foo
Hello
$ python3 -Im foo
/usr/bin/python3: No module named foo However, I'm flagging this as an enhancement request for 3.8+ (with a reworded issue title), as the non-isolated
A potentially related enhancement would be to modify directory & zipfile execution to only look for |
-m
will add empty directory to sys.path-m
Also, a small upstream community interaction tip: if you want people to seriously consider your requests for changes in default behaviour (which inevitably risk backwards compatibility breaks), don't start out by insulting them. Python's defaults are currently set up for a *trusted personal automation tool*, where the person writing the code is also the person running it. By starting out with an insult like "I think this is a really stupid security bug", you're actually saying "I know very little about Python's history, or the audiences it was originally written to serve, and instead of politely suggesting an alternative behaviour that would be more robust in the face of system configuration errors, I'm going to try to use shame, guilt, and embarrassment to get people to do work for me". That kind of behaviour *isn't* a good way to get your issues addressed, but it *is* a good way to encourage people to decide that volunteering as an open source maintainer isn't worth the associated hassles. The opening insult added nothing to your issue report, and could more productively have been replaced with an explanation of the expectations you had of the default behaviour, how you came by those expectations, and how the current behaviour failed to meet them. |
I've also separated out https://bugs.python.org/issue33095 (a docs issue about making isolated mode more discoverable) based on the jwilk's comment that it wasn't clear how to disable the default "add the current directory to sys.path" behaviour. |
Whoa, wait, what? I agree that the original post is not as diplomatic as it could be, but my reaction to learning about this just now is also shock and confusion, so I guess I can sympathize with the OP a bit... The reason I'm surprised is that -- while this probably wasn't fully anticipated when -m was designed -- it's turned out to be a bit of a meme to replace calls like 'pip ...' with 'python -m pip ...', or 'virtualenv ...' with 'python -m virtualenv ...', etc. I thought these were generally pretty much equivalent. I definitely did *not* know that running 'python -m pip' could lead to executing arbitrary code from the cwd, and I'm sure I've run it inside e.g. random git checkouts. If someone had tried to spearphish me with this they would totally have succeeded. (I hope they haven't?) If you want to run a file in the current directory, is there any advantage to doing 'python -m myscript' instead of 'python myscript.py'? Could we declare that the latter is the One Obvious Way and remove support for the former entirely? |
"python -m mypkg.myscript" does the right thing as far as local packages are concerned, whereas "python -m mypkg/myscript.py" will set you up for double-import bugs. Note that you can almost always trigger arbitrary non-obvious code execution just by writing sitecustomize.py to the current directory, and any package you install can add a "<installation-site-packages>/arbitrary-code.pth" or "<user-site-packages>/arbitrary-code.pth" file that gets run at startup (setuptools has long relied on this to implement various features). Opting in to isolated mode turns *all* of those features off by saying "I'm expecting to run system code only here, not custom user code". |
-I implies -s, which is not something I want. |
https://bugs.python.org/issue13475 is the existing enhancement request to expose sys.path[0] management independently of the other execution isolation features. |
@ncoghlan: The comparison I'm worried about is specifically this one: IIUC, right now it's safe to run 'pip --version' in an arbitrary directory, but it's not safe to run 'python -m pip --version' in an arbitrary directory. Am I wrong? (I actually couldn't convince either version to execute arbitrary code in 2 minutes of trying, but that's my understanding of the discussion so far.) If that's correct, then I don't think this is like... the hugest security bug ever, but... I also think that it's irresponsible for e.g. packaging.python.org to be recommending people run 'python -m pip' the way it does now, and we need to find some way to change things so our beginner docs aren't triggering arbitrary code execution in a rare and subtle case. We could add a bunch of warnings to packaging.python.org, explaining about how the code execution can be triggered, but that seems unsatisfactory given how those docs are targeted at beginners, plus there are so many places around the internet that recommend 'python -m pip' we'd never find them all. We could update all those docs to instead recommend 'python -Im pip', but that has the same problems: no-one will understand, and people won't do it. We could stop recommending 'python -m pip' entirely, but that runs into all the problems that have motivated this in the first place. So I think we should find a way to make it so 'python -m pip' *never* executes code from the current directory (modulo the usual caveats, like the user explicitly setting PYTHONPATH to an insecure value etc.). If 'python -m mypkg.myscript' is important, maybe we can make it 'PYTHONPATH=. python -m mypkg.myscript', or 'python -M mypkg.myscript', or making 'python mypkg/myscript.py' DTRT, or... something? |
Took 2 seconds. % sudo python3 -mpip --version
hello world
Traceback (most recent call last):
File "/usr/lib/python3.6/runpy.py", line 183, in _run_module_as_main
mod_name, mod_spec, code = _get_module_details(mod_name, _Error)
File "/usr/lib/python3.6/runpy.py", line 142, in _get_module_details
return _get_module_details(pkg_main_name, error)
File "/usr/lib/python3.6/runpy.py", line 109, in _get_module_details
__import__(pkg_name)
File "/usr/lib/python3/dist-packages/pip/__init__.py", line 4, in <module>
import locale
File "/usr/lib/python3.6/locale.py", line 180, in <module>
_percent_re = re.compile(r'%(?:\((?P<key>.*?)\))?'
AttributeError: module 're' has no attribute 'compile'
Error in sys.excepthook:
Traceback (most recent call last):
File "/usr/lib/python3/dist-packages/apport_python_hook.py", line 53, in apport_excepthook
if not enabled():
File "/usr/lib/python3/dist-packages/apport_python_hook.py", line 28, in enabled
return re.search(r'^\s*enabled\s*=\s*0\s*$', conf, re.M) is None
AttributeError: module 're' has no attribute 'search'
Original exception was:
Traceback (most recent call last):
File "/usr/lib/python3.6/runpy.py", line 183, in _run_module_as_main
mod_name, mod_spec, code = _get_module_details(mod_name, _Error)
File "/usr/lib/python3.6/runpy.py", line 142, in _get_module_details
return _get_module_details(pkg_main_name, error)
File "/usr/lib/python3.6/runpy.py", line 109, in _get_module_details
__import__(pkg_name)
File "/usr/lib/python3/dist-packages/pip/__init__.py", line 4, in <module>
import locale
File "/usr/lib/python3.6/locale.py", line 180, in <module>
_percent_re = re.compile(r'%(?:\((?P<key>.*?)\))?'
AttributeError: module 're' has no attribute 'compile' Same for ---- I'd prefer there be a change that the default be always safe from some version on, so that the REPL can do whatever it does, but |
Ah, yeah, I see: ~/t$ echo 'print("hi")' > re.py
~/t$ pip --version
pip 9.0.1 from /home/njs/.user-python3.5-64bit/local/lib/python3.5/site-packages (python 3.5)
~/t$ python -m pip --version
hi
Traceback (most recent call last):
[...] But if I create a sitecustomize.py or an io.py in the current directory (knowing that 'import io' happens implicitly startup), then those *don't* seem to get picked up by 'python -m pip' or 'python -c ...' or plain 'python'. I guess the cwd doesn't get added to sys.path until after initial bootstrapping is finished. |
It occurs to me that there may be some additional unshared context here: the way This behaviour means that if you want to intercept "python -m pip", the current easiest filename to use to intercept it is just "pip.py", similar to the way you can use "pip.exe" or "python.exe" to intercept those commands on Windows. I do think switching from a Windows-style default search behaviour to a *nix style default search behaviour is potentially reasonable, but the related backwards compatibility considerations would likely push it into being a PEP level change. I'll also note that Nathaniel's right that I was wrong about I was just confusing myself, as my default approach to double-checking the behaviour of the "-m" switch is to run "python -m site", but that's misleading in this case since doing that also re-runs the site and user customisation modules (and will pick them up from the current working directory) - it's closely akin to testing |
That's classic Windows behavior. However, search paths for CreateProcess and the loader are composed on demand, which allows different behavior to be selected easily enough. The current behavior depends on a mix of environment variables, registry settings, reserved names (known DLLs, API sets), application and DLL manifests, .local redirection, and in-process configuration. For example, to skip the working directory when searching for DLLs, there's the CWDIllegalInDllSearch registry setting, SetDllDirectoryW(L""), or SetDefaultDllDirectories (the latter removes PATH as well, so it's not suited for loosely-couple systems). To skip the working directory when searching for executables, define the environment variable "NoDefaultCurrentDirectoryInExePath". |
(Adding the other import system maintainers to the nosy list, along with Ned as the release manager for 3.6 and 3.7) Summarizing my current thoughts on this: I think the fact that "-m" currently adds the empty directory as sys.path[0] instead of the fully resolved cwd is definitely worth changing for 3.8, and is at least arguably a bugfix that should be applied to 3.7 prior to release. (It's probably too late in 3.6's lifecycle to be worth backporting that far, even if we do reclassify this part as a bugfix instead of an enhancement) The above is what I consider to be in scope for *this* particular issue. (The details of the current behaviour probably arose from my borrowing an existing code path originally designed for "-c" rather than from a deliberate design decision) As a separate question, I'd personally be open to the idea of:
However, that second part is the part I consider out of scope for this issue, and think would require a PEP due to the backwards compatibility challenges. |
The proposed change for 3.8 and 3.7 seems reasonable to me. |
I am OK with changing '' to the initial resolved working directory for 3.7 as long as we get that change in now, i.e. before the 3.7.0b3 ABI freeze next Monday. And I agree that we should not attempt to change this behavior for 3.6.x at this stage of its life cycle. |
Brett or Eric, any chance one of you could run with this for 3.7b3? I already have a startup refactoring related regression that I'm aiming to have fixed before then. Thanks to Victor's refactoring work, there's at least a clear interception point now where we can treat the empty string differently depending on whether the command line option was As an initial attempt, I think the necessary fix will be along the lines of checking for 'n == 0 && argc > 1 && wcscmp(argv0, L"-m") == 0', and resolving the current working directory in that case. |
I can't make any promises unfortunately. |
PR posted with the change to use an absolute path for the starting working directory in the "-m" case. That PR also includes a change to improve the fidelity of the test suite: back when I first wrote test_cmd_line_script, I was mainly focused on testing the runpy aspects, and not the sys.path initialisation aspects, so the way the tests worked didn't really check the latter properly. The test updates get rid of the launch script that was previously confusing matters, and instead test sys.path[0] initialisation properly (relying on |
It turned out some tests in CPython's own test suite were implicitly relying on the old behaviour where the current working directory automatically ended up on sys.path (see the changes to test_bdb and test_doctest in the updated PR). |
Initial fix has been merged to master, CI runs pending for the backport to 3.7 and a follow-up master branch PR to remove a debugging print I noticed when resolving a test_import conflict in the backport. I won't get to merging those until some time after work tomorrow (probably 8 pm'ish in UTC+10), so if anyone wanted to merge them before that, it would likely be a good idea :) |
3.7 CI finished before I logged off for the night, so this is good to go for 3.7.0b3 now :) |
Marking as fixed, since this is now the version likely to go out in 3.7.0b3 - if we find further problems with it (beyond the potential enhancement discussed above to make local directory usage opt-in), then those can go in a new issue. |
(See bpo-33185 for regression.) |
Some notes from my investigation of bpo-33185 that seem more appropriate here, rather than on that issue:
Aside from switching pydoc from a literal '.' to os.curdir, I'm not going to change any of those (hence why I'm putting these notes here), but I wanted to capture this info in case does decide to follow through on a "less isolated than isolated mode, but still omits the current directory from sys.path" execution mode. |
Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.
Show more details
GitHub fields:
bugs.python.org fields:
The text was updated successfully, but these errors were encountered: