Safeguard
Security Guides•

Python subprocess shell=True: when is it ever acceptable?

Passing a list to subprocess avoids the shell and shell injection. When shell=True is still needed, how to quote input, and the Windows batch file caveat.

Safeguard Security Team
3 min read

Almost never with untrusted input. Passing a list of arguments to subprocess.run keeps the shell out of the picture, so metacharacters stay plain text. If you set shell=True, the Python docs say it is your job to quote every value correctly to avoid shell injection.

What the shell adds

With a list such as ["cat", filename], Python starts the program directly. A filename like report; rm -rf ~ is just one odd filename.

With shell=True and a string, a system shell parses the command. Semicolons, pipes, backticks, $() and redirection now mean something. The docs say the library does not implicitly choose to call a shell, so all characters, including shell metacharacters, can safely be passed to child processes when you do not ask for one.

import subprocess

filename = "report; rm -rf ~.txt"   # untrusted

subprocess.run(["cat", filename])                # safe form
# subprocess.run(f"cat {filename}", shell=True)  # shell injection

When people reach for shell=True

Common reasons, and what to do instead:

  • Pipes. Run two processes and connect them with stdout=subprocess.PIPE, or do the second step in Python.
  • Globbing. Use glob or pathlib to expand patterns, then pass the resulting list.
  • Environment variables in the string. Use the env argument.
  • Redirecting output to a file. Use the stdout argument with an open file.
  • Copying a command from a terminal. Split it once with shlex.split on a trusted string and keep it as a list.

If none of those apply to your case, it is a good sign you did not need the shell.

If you truly need a shell

Some tasks, such as a trusted one line admin snippet from your own config, do use shell features. For those cases:

  • Never build the string from user input.
  • If a value must be included, wrap it with shlex.quote. The docs note this works on some platforms, so read the platform warning in the shlex docs.
  • Keep the command text constant and pass variable parts through environment variables that the command reads.
  • Avoid giving the child more privilege than the parent needs.

The Windows batch file caveat

The docs add an easy to miss point: on Windows, .bat and .cmd files may be launched through a system shell regardless of the arguments you pass. That means arguments can be parsed by shell rules without Python adding escaping. If you must run a batch file with untrusted arguments, the docs suggest considering shell=True so that Python escapes special characters. The safer route is to avoid batch files for this and call the executable directly.

Review tip

Search for shell=True, os.system and os.popen. For each hit, ask whether any part of the string can come from outside your code, including filenames, branch names and environment values. If so, convert it to a list.

For related reading see command injection prevention in Python. Safeguard's dependency scanning flags vulnerable libraries that wrap command execution, and you can run a free scan at gold.safeguard.sh/scan.

Quick check

Is shlex.quote enough to make shell=True safe?

It helps for values you interpolate, but the docs say it works on some platforms. Using a list avoids the question.

Does a list guarantee safety?

It prevents shell injection. A user supplied value can still be an unexpected option such as a leading dash, so validate or put a double dash separator before it where the program supports it.

Sources

Last reviewed: 19 October 2026.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.