Avoid input from untrusted sources to be used directly as part of an OS command and use specialized Python modules where possible instead.
Python can run shell commands either with an active shell=True
where an actual shell is invoked to run a line of commands such /bin/bash -c "ls -la *.txt"
or via non-interactive shell=False
expecting a Python list object.
Using shell=False
is recommended but is not going to prevent all attacks.
Examples of reduced functionality with shell=False
:
ls -1 *.txt
get surrounded by single quotes ls -1 '*.txt'
so that some Unix commands to no longer work.ls -1 |grep *.txt
is prohibited.Specialized Python modules, such as pathlib
or shutil
, provide a platform-independent solution for most needs and should generally be preferred.
Following table 00 provides a limited list of Unix shell commands to Python module mapping, see Python Module index for more.
Action | Unix | Python |
---|---|---|
Compress or decompress files | gzip, unzip | zlib, gzip, bz2, lzma |
Filesystem operations | find . tree ls -1 *.txt test -d test -f cp |
Path.rglob("*.txt") Path.glob("*.txt") Path.is_dir() Path.is_file() shutil.copy() |
Access control operations | chown chmod |
shutil.chown() shutil.chmod() stat |
Environment variables | export set |
os.getenv() os.setenv() |
Get user/group id | id | os.getuid() os.setuid() |
Get OS and/or kernel type and name | uname -as | os.uname() |
table 00, example list of Unix commands and their Python equivalents.
Any variation of using input from a lesser trusted source as part of a command line program has a very high probability of resulting in a potential attack including the use of specialized modules. Consider:
This scenario demonstrates a potential remote command execution. The FileOperations.list_dir()
method allows an attacker to inject commands into the string dirname such as head -1 /etc/passwd
under Linux or net user
under Windows. Older versions of Python < 3.9.12
allow to turn a non-interactive shell into an active shell in Windows by providing cmd.exe /C
as an argument [python.org 3.12.5 - Subprocess management].
# SPDX-FileCopyrightText: OpenSSF project contributors
# SPDX-License-Identifier: MIT
""" Non-compliant Code Example """
from subprocess import Popen
import os
class FileOperations:
"""Helper class for file system operations"""
def list_dir(self, dirname: str):
"""List the contents of a directory"""
if "nt" in os.name:
Popen("dir " + dirname, shell=True).communicate()
if "posix" in os.name:
Popen("ls " + dirname, shell=True).communicate()
#####################
# Trying to exploit above code example
#####################
if "nt" in os.name:
FileOperations().list_dir("%HOMEPATH% & net user")
if "posix" in os.name:
FileOperations().list_dir("/etc/shadow; head -1 /etc/passwd")
The code in noncompliant01.py
prints the first line of /etc/passwd
on Linux or starts net user
under Windows.
The FileOperations().list_dir()
method allows an attacker to add commands via ;
in Linux and &
in Windows.
The attack surface increases if a user is also allowed to upload or create files or folders.
The noncompliant02.py
example demonstrates the injection via file or folder name that is created prior to using the list_dir()
method. We assume here that an untrusted user is allowed to create files or folders named & calc.exe or ;ps aux
as part of another service such as upload area, submit form, or as a result of a zip-bomb as per CWE-409: Improper Handling of Highly Compressed Data (Data Amplification). Encoding issues as described in CWE-180: Incorrect Behavior Order: Validate Before Canonicalize must also be considered.
The issue occurs when mixing shell commands with data from a lesser trusted source.
Some shell commands, such as find
with -exec
, allow running secondary commands via arguments [CTFOBins] [LOLBAS] that can be misused for shell injections if no shell is provided shell=False
. The shlex.split()
method is frequently used to split a string into a list for subprocess.run()
in order to run a non-interactive shell such as ls -la
into ["ls", "-la"]
and plays a minor role in simplifying the attack. The noncompliant02.py
code only works on Linux, in this example calling a rather harmless uptime.
# SPDX-FileCopyrightText: OpenSSF project contributors
# SPDX-License-Identifier: MIT
""" Non-compliant Code Example """
import os
import shlex
from subprocess import run
def list_dir(dirname: str):
"""Lists only 2 levels of folders in a default directory"""
os.chdir(dirname)
cmd = "find . -maxdepth 1 -type d"
result = run(shlex.split(cmd), check=True, capture_output=True)
for subfolder in result.stdout.decode("utf-8").splitlines():
cmd = "find " + subfolder + " -maxdepth 1 -type d"
subresult = run(shlex.split(cmd), check=True, capture_output=True)
for item in subresult.stdout.decode("utf-8").splitlines():
print(item)
#####################
# Trying to exploit above code example
#####################
# just to keep it clean we create folder for this test
os.makedirs("temp", exist_ok=True)
# simulating upload area (payload):
print("Testing Corrupted Directory")
if "posix" in os.name:
with open("temp/toast.sh", "w", encoding="utf-8") as file_handle:
file_handle.write("uptime\n")
os.makedirs("temp/. -exec bash toast.sh {} +", exist_ok=True)
# running the query:
list_dir("temp")
In noncompliant02.py
the attacker creates a toast.sh
file that contains the commands to run. The attacker also creates a folder named . -exec bash toast.sh {} +
that will later become part of the shell find
command forming find . -exec bash toast.sh {} +
.
The result is that list_dir(dirname)
will run the toast.sh
as a shell script. The toast.sh
file does not require execute rights and can contain any quantity of shell command complexity.
The compliant01.py
code using the cross-platform compatible pathlib module and restricting filesystem area. The pathlib
on its own will not prevent all attacks.
# SPDX-FileCopyrightText: OpenSSF project contributors
# SPDX-License-Identifier: MIT
""" Compliant Code Example """
import os
from pathlib import Path
def list_dir(dirname: str):
"""List the contents of a directory recursively
Parameters:
dirname (string): Directory name
"""
path = Path(dirname)
allowed_directory = Path.home()
# TODO: input sanitation
# TODO: Add secure logging
if Path(
allowed_directory.joinpath(dirname)
.resolve()
.relative_to(allowed_directory.resolve())
):
for item in path.glob("*"):
print(item)
#####################
# Trying to exploit above code example
#####################
# just to keep it clean we create folder for this test
os.makedirs("temp", exist_ok=True)
# simulating upload area (payload):
print("Testing Corrupted Directory")
if "posix" in os.name:
with open("temp/toast.sh", "w", encoding="utf-8") as file_handle:
file_handle.write("uptime\n")
os.makedirs("temp/. -exec bash toast.sh {} +", exist_ok=True)
# running the query:
list_dir("temp")
The compliant01.py
does not use data that origins from a lesser trusted source in order to form a shell command and would throw an error for an attempt to list content outside of the allowed area. The code is actually not “neutralizing” data itself from an untrusted source as such, the attack is “neutralized” by no longer using subprocess
or os
to run find
.
Tool | Version | Checker | Description |
---|---|---|---|
Pycharm | 2022.3.3 Python 3.11.6 | PR100 | Calling subprocess.call , subprocess.run , or subprocess.Popen with shell=True can leave the host shell open to local code execution or remote code execution attacks |
bandit | 1.7.9 on python 3.11.4 | B404 | Consider possible security implications associated with these modules |
bandit | 1.7.9 on python 3.11.4 | B602 | Bsubprocess call with shell=True identified, security issue.bandit |
bandit | 1.7.9 on python 3.11.4 | B603 | subprocess call - check for execution of untrusted input. |
bandit | 1.7.9 on python 3.11.4 | B604 | Consider possible security implications associated with the subprocess module |
bandit | 1.7.9 on python 3.11.4 | B605 | Bandit seems to detect any use of os.system() whether sanitized or not. |
[Python docs] | subprocess — Subprocess management — Python 3.10.4 documentation [online]. Available from: https://docs.python.org/3/library/subprocess.html, [accessed 1 November 2024] |
[Python docs] | os — Miscellaneous operating system interfaces — Python 3.10.4 documentation [online]. Available from: https://docs.python.org/3/library/os.html#os.system, [accessed 1 November 2024] |
[CTFOBins] | GTFOBins is a curated list of Unix binaries that can be used to bypass local security restrictions in misconfigured systems. [online]. Available from: https://gtfobins.github.io/, [accessed 1 November 2024] |
[LOLBAS] | LOLBAS Living Off The Land Binaries, Scripts and Libraries. [online]. Available from: https://lolbas-project.github.io/, [accessed 1 November 2024] |