| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694 |
- #
- # Copyright 2014 Hewlett-Packard Development Company, L.P.
- #
- # SPDX-License-Identifier: Apache-2.0
- import ast
- import re
- import bandit
- from bandit.core import issue
- from bandit.core import test_properties as test
- # yuck, regex: starts with a windows drive letter (eg C:)
- # or one of our path delimeter characters (/, \, .)
- full_path_match = re.compile(r"^(?:[A-Za-z](?=\:)|[\\\/\.])")
- def _evaluate_shell_call(context):
- no_formatting = isinstance(context.node.args[0], ast.Str)
- if no_formatting:
- return bandit.LOW
- else:
- return bandit.HIGH
- def gen_config(name):
- if name == "shell_injection":
- return {
- # Start a process using the subprocess module, or one of its
- # wrappers.
- "subprocess": [
- "subprocess.Popen",
- "subprocess.call",
- "subprocess.check_call",
- "subprocess.check_output",
- "subprocess.run",
- ],
- # Start a process with a function vulnerable to shell injection.
- "shell": [
- "os.system",
- "os.popen",
- "os.popen2",
- "os.popen3",
- "os.popen4",
- "popen2.popen2",
- "popen2.popen3",
- "popen2.popen4",
- "popen2.Popen3",
- "popen2.Popen4",
- "commands.getoutput",
- "commands.getstatusoutput",
- ],
- # Start a process with a function that is not vulnerable to shell
- # injection.
- "no_shell": [
- "os.execl",
- "os.execle",
- "os.execlp",
- "os.execlpe",
- "os.execv",
- "os.execve",
- "os.execvp",
- "os.execvpe",
- "os.spawnl",
- "os.spawnle",
- "os.spawnlp",
- "os.spawnlpe",
- "os.spawnv",
- "os.spawnve",
- "os.spawnvp",
- "os.spawnvpe",
- "os.startfile",
- ],
- }
- def has_shell(context):
- keywords = context.node.keywords
- result = False
- if "shell" in context.call_keywords:
- for key in keywords:
- if key.arg == "shell":
- val = key.value
- if isinstance(val, ast.Num):
- result = bool(val.n)
- elif isinstance(val, ast.List):
- result = bool(val.elts)
- elif isinstance(val, ast.Dict):
- result = bool(val.keys)
- elif isinstance(val, ast.Name) and val.id in ["False", "None"]:
- result = False
- elif isinstance(val, ast.NameConstant):
- result = val.value
- else:
- result = True
- return result
- @test.takes_config("shell_injection")
- @test.checks("Call")
- @test.test_id("B602")
- def subprocess_popen_with_shell_equals_true(context, config):
- """**B602: Test for use of popen with shell equals true**
- Python possesses many mechanisms to invoke an external executable. However,
- doing so may present a security issue if appropriate care is not taken to
- sanitize any user provided or variable input.
- This plugin test is part of a family of tests built to check for process
- spawning and warn appropriately. Specifically, this test looks for the
- spawning of a subprocess using a command shell. This type of subprocess
- invocation is dangerous as it is vulnerable to various shell injection
- attacks. Great care should be taken to sanitize all input in order to
- mitigate this risk. Calls of this type are identified by a parameter of
- 'shell=True' being given.
- Additionally, this plugin scans the command string given and adjusts its
- reported severity based on how it is presented. If the command string is a
- simple static string containing no special shell characters, then the
- resulting issue has low severity. If the string is static, but contains
- shell formatting characters or wildcards, then the reported issue is
- medium. Finally, if the string is computed using Python's string
- manipulation or formatting operations, then the reported issue has high
- severity. These severity levels reflect the likelihood that the code is
- vulnerable to injection.
- See also:
- - :doc:`../plugins/linux_commands_wildcard_injection`
- - :doc:`../plugins/subprocess_without_shell_equals_true`
- - :doc:`../plugins/start_process_with_no_shell`
- - :doc:`../plugins/start_process_with_a_shell`
- - :doc:`../plugins/start_process_with_partial_path`
- **Config Options:**
- This plugin test shares a configuration with others in the same family,
- namely `shell_injection`. This configuration is divided up into three
- sections, `subprocess`, `shell` and `no_shell`. They each list Python calls
- that spawn subprocesses, invoke commands within a shell, or invoke commands
- without a shell (by replacing the calling process) respectively.
- This plugin specifically scans for methods listed in `subprocess` section
- that have shell=True specified.
- .. code-block:: yaml
- shell_injection:
- # Start a process using the subprocess module, or one of its
- wrappers.
- subprocess:
- - subprocess.Popen
- - subprocess.call
- :Example:
- .. code-block:: none
- >> Issue: subprocess call with shell=True seems safe, but may be
- changed in the future, consider rewriting without shell
- Severity: Low Confidence: High
- CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
- Location: ./examples/subprocess_shell.py:21
- 20 subprocess.check_call(['/bin/ls', '-l'], shell=False)
- 21 subprocess.check_call('/bin/ls -l', shell=True)
- 22
- >> Issue: call with shell=True contains special shell characters,
- consider moving extra logic into Python code
- Severity: Medium Confidence: High
- CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
- Location: ./examples/subprocess_shell.py:26
- 25
- 26 subprocess.Popen('/bin/ls *', shell=True)
- 27 subprocess.Popen('/bin/ls %s' % ('something',), shell=True)
- >> Issue: subprocess call with shell=True identified, security issue.
- Severity: High Confidence: High
- CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
- Location: ./examples/subprocess_shell.py:27
- 26 subprocess.Popen('/bin/ls *', shell=True)
- 27 subprocess.Popen('/bin/ls %s' % ('something',), shell=True)
- 28 subprocess.Popen('/bin/ls {}'.format('something'), shell=True)
- .. seealso::
- - https://security.openstack.org
- - https://docs.python.org/3/library/subprocess.html#frequently-used-arguments
- - https://security.openstack.org/guidelines/dg_use-subprocess-securely.html
- - https://security.openstack.org/guidelines/dg_avoid-shell-true.html
- - https://cwe.mitre.org/data/definitions/78.html
- .. versionadded:: 0.9.0
- .. versionchanged:: 1.7.3
- CWE information added
- """ # noqa: E501
- if config and context.call_function_name_qual in config["subprocess"]:
- if has_shell(context):
- if len(context.call_args) > 0:
- sev = _evaluate_shell_call(context)
- if sev == bandit.LOW:
- return bandit.Issue(
- severity=bandit.LOW,
- confidence=bandit.HIGH,
- cwe=issue.Cwe.OS_COMMAND_INJECTION,
- text="subprocess call with shell=True seems safe, but "
- "may be changed in the future, consider "
- "rewriting without shell",
- lineno=context.get_lineno_for_call_arg("shell"),
- )
- else:
- return bandit.Issue(
- severity=bandit.HIGH,
- confidence=bandit.HIGH,
- cwe=issue.Cwe.OS_COMMAND_INJECTION,
- text="subprocess call with shell=True identified, "
- "security issue.",
- lineno=context.get_lineno_for_call_arg("shell"),
- )
- @test.takes_config("shell_injection")
- @test.checks("Call")
- @test.test_id("B603")
- def subprocess_without_shell_equals_true(context, config):
- """**B603: Test for use of subprocess without shell equals true**
- Python possesses many mechanisms to invoke an external executable. However,
- doing so may present a security issue if appropriate care is not taken to
- sanitize any user provided or variable input.
- This plugin test is part of a family of tests built to check for process
- spawning and warn appropriately. Specifically, this test looks for the
- spawning of a subprocess without the use of a command shell. This type of
- subprocess invocation is not vulnerable to shell injection attacks, but
- care should still be taken to ensure validity of input.
- Because this is a lesser issue than that described in
- `subprocess_popen_with_shell_equals_true` a LOW severity warning is
- reported.
- See also:
- - :doc:`../plugins/linux_commands_wildcard_injection`
- - :doc:`../plugins/subprocess_popen_with_shell_equals_true`
- - :doc:`../plugins/start_process_with_no_shell`
- - :doc:`../plugins/start_process_with_a_shell`
- - :doc:`../plugins/start_process_with_partial_path`
- **Config Options:**
- This plugin test shares a configuration with others in the same family,
- namely `shell_injection`. This configuration is divided up into three
- sections, `subprocess`, `shell` and `no_shell`. They each list Python calls
- that spawn subprocesses, invoke commands within a shell, or invoke commands
- without a shell (by replacing the calling process) respectively.
- This plugin specifically scans for methods listed in `subprocess` section
- that have shell=False specified.
- .. code-block:: yaml
- shell_injection:
- # Start a process using the subprocess module, or one of its
- wrappers.
- subprocess:
- - subprocess.Popen
- - subprocess.call
- :Example:
- .. code-block:: none
- >> Issue: subprocess call - check for execution of untrusted input.
- Severity: Low Confidence: High
- CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
- Location: ./examples/subprocess_shell.py:23
- 22
- 23 subprocess.check_output(['/bin/ls', '-l'])
- 24
- .. seealso::
- - https://security.openstack.org
- - https://docs.python.org/3/library/subprocess.html#frequently-used-arguments
- - https://security.openstack.org/guidelines/dg_avoid-shell-true.html
- - https://security.openstack.org/guidelines/dg_use-subprocess-securely.html
- - https://cwe.mitre.org/data/definitions/78.html
- .. versionadded:: 0.9.0
- .. versionchanged:: 1.7.3
- CWE information added
- """ # noqa: E501
- if config and context.call_function_name_qual in config["subprocess"]:
- if not has_shell(context):
- return bandit.Issue(
- severity=bandit.LOW,
- confidence=bandit.HIGH,
- cwe=issue.Cwe.OS_COMMAND_INJECTION,
- text="subprocess call - check for execution of untrusted "
- "input.",
- lineno=context.get_lineno_for_call_arg("shell"),
- )
- @test.takes_config("shell_injection")
- @test.checks("Call")
- @test.test_id("B604")
- def any_other_function_with_shell_equals_true(context, config):
- """**B604: Test for any function with shell equals true**
- Python possesses many mechanisms to invoke an external executable. However,
- doing so may present a security issue if appropriate care is not taken to
- sanitize any user provided or variable input.
- This plugin test is part of a family of tests built to check for process
- spawning and warn appropriately. Specifically, this plugin test
- interrogates method calls for the presence of a keyword parameter `shell`
- equalling true. It is related to detection of shell injection issues and is
- intended to catch custom wrappers to vulnerable methods that may have been
- created.
- See also:
- - :doc:`../plugins/linux_commands_wildcard_injection`
- - :doc:`../plugins/subprocess_popen_with_shell_equals_true`
- - :doc:`../plugins/subprocess_without_shell_equals_true`
- - :doc:`../plugins/start_process_with_no_shell`
- - :doc:`../plugins/start_process_with_a_shell`
- - :doc:`../plugins/start_process_with_partial_path`
- **Config Options:**
- This plugin test shares a configuration with others in the same family,
- namely `shell_injection`. This configuration is divided up into three
- sections, `subprocess`, `shell` and `no_shell`. They each list Python calls
- that spawn subprocesses, invoke commands within a shell, or invoke commands
- without a shell (by replacing the calling process) respectively.
- Specifically, this plugin excludes those functions listed under the
- subprocess section, these methods are tested in a separate specific test
- plugin and this exclusion prevents duplicate issue reporting.
- .. code-block:: yaml
- shell_injection:
- # Start a process using the subprocess module, or one of its
- wrappers.
- subprocess: [subprocess.Popen, subprocess.call,
- subprocess.check_call, subprocess.check_output
- execute_with_timeout]
- :Example:
- .. code-block:: none
- >> Issue: Function call with shell=True parameter identified, possible
- security issue.
- Severity: Medium Confidence: High
- CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
- Location: ./examples/subprocess_shell.py:9
- 8 pop('/bin/gcc --version', shell=True)
- 9 Popen('/bin/gcc --version', shell=True)
- 10
- .. seealso::
- - https://security.openstack.org/guidelines/dg_avoid-shell-true.html
- - https://security.openstack.org/guidelines/dg_use-subprocess-securely.html
- - https://cwe.mitre.org/data/definitions/78.html
- .. versionadded:: 0.9.0
- .. versionchanged:: 1.7.3
- CWE information added
- """ # noqa: E501
- if config and context.call_function_name_qual not in config["subprocess"]:
- if has_shell(context):
- return bandit.Issue(
- severity=bandit.MEDIUM,
- confidence=bandit.LOW,
- cwe=issue.Cwe.OS_COMMAND_INJECTION,
- text="Function call with shell=True parameter identified, "
- "possible security issue.",
- lineno=context.get_lineno_for_call_arg("shell"),
- )
- @test.takes_config("shell_injection")
- @test.checks("Call")
- @test.test_id("B605")
- def start_process_with_a_shell(context, config):
- """**B605: Test for starting a process with a shell**
- Python possesses many mechanisms to invoke an external executable. However,
- doing so may present a security issue if appropriate care is not taken to
- sanitize any user provided or variable input.
- This plugin test is part of a family of tests built to check for process
- spawning and warn appropriately. Specifically, this test looks for the
- spawning of a subprocess using a command shell. This type of subprocess
- invocation is dangerous as it is vulnerable to various shell injection
- attacks. Great care should be taken to sanitize all input in order to
- mitigate this risk. Calls of this type are identified by the use of certain
- commands which are known to use shells. Bandit will report a LOW
- severity warning.
- See also:
- - :doc:`../plugins/linux_commands_wildcard_injection`
- - :doc:`../plugins/subprocess_without_shell_equals_true`
- - :doc:`../plugins/start_process_with_no_shell`
- - :doc:`../plugins/start_process_with_partial_path`
- - :doc:`../plugins/subprocess_popen_with_shell_equals_true`
- **Config Options:**
- This plugin test shares a configuration with others in the same family,
- namely `shell_injection`. This configuration is divided up into three
- sections, `subprocess`, `shell` and `no_shell`. They each list Python calls
- that spawn subprocesses, invoke commands within a shell, or invoke commands
- without a shell (by replacing the calling process) respectively.
- This plugin specifically scans for methods listed in `shell` section.
- .. code-block:: yaml
- shell_injection:
- shell:
- - os.system
- - os.popen
- - os.popen2
- - os.popen3
- - os.popen4
- - popen2.popen2
- - popen2.popen3
- - popen2.popen4
- - popen2.Popen3
- - popen2.Popen4
- - commands.getoutput
- - commands.getstatusoutput
- :Example:
- .. code-block:: none
- >> Issue: Starting a process with a shell: check for injection.
- Severity: Low Confidence: Medium
- CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
- Location: examples/os_system.py:3
- 2
- 3 os.system('/bin/echo hi')
- .. seealso::
- - https://security.openstack.org
- - https://docs.python.org/3/library/os.html#os.system
- - https://docs.python.org/3/library/subprocess.html#frequently-used-arguments
- - https://security.openstack.org/guidelines/dg_use-subprocess-securely.html
- - https://cwe.mitre.org/data/definitions/78.html
- .. versionadded:: 0.10.0
- .. versionchanged:: 1.7.3
- CWE information added
- """ # noqa: E501
- if config and context.call_function_name_qual in config["shell"]:
- if len(context.call_args) > 0:
- sev = _evaluate_shell_call(context)
- if sev == bandit.LOW:
- return bandit.Issue(
- severity=bandit.LOW,
- confidence=bandit.HIGH,
- cwe=issue.Cwe.OS_COMMAND_INJECTION,
- text="Starting a process with a shell: "
- "Seems safe, but may be changed in the future, "
- "consider rewriting without shell",
- )
- else:
- return bandit.Issue(
- severity=bandit.HIGH,
- confidence=bandit.HIGH,
- cwe=issue.Cwe.OS_COMMAND_INJECTION,
- text="Starting a process with a shell, possible injection"
- " detected, security issue.",
- )
- @test.takes_config("shell_injection")
- @test.checks("Call")
- @test.test_id("B606")
- def start_process_with_no_shell(context, config):
- """**B606: Test for starting a process with no shell**
- Python possesses many mechanisms to invoke an external executable. However,
- doing so may present a security issue if appropriate care is not taken to
- sanitize any user provided or variable input.
- This plugin test is part of a family of tests built to check for process
- spawning and warn appropriately. Specifically, this test looks for the
- spawning of a subprocess in a way that doesn't use a shell. Although this
- is generally safe, it maybe useful for penetration testing workflows to
- track where external system calls are used. As such a LOW severity message
- is generated.
- See also:
- - :doc:`../plugins/linux_commands_wildcard_injection`
- - :doc:`../plugins/subprocess_without_shell_equals_true`
- - :doc:`../plugins/start_process_with_a_shell`
- - :doc:`../plugins/start_process_with_partial_path`
- - :doc:`../plugins/subprocess_popen_with_shell_equals_true`
- **Config Options:**
- This plugin test shares a configuration with others in the same family,
- namely `shell_injection`. This configuration is divided up into three
- sections, `subprocess`, `shell` and `no_shell`. They each list Python calls
- that spawn subprocesses, invoke commands within a shell, or invoke commands
- without a shell (by replacing the calling process) respectively.
- This plugin specifically scans for methods listed in `no_shell` section.
- .. code-block:: yaml
- shell_injection:
- no_shell:
- - os.execl
- - os.execle
- - os.execlp
- - os.execlpe
- - os.execv
- - os.execve
- - os.execvp
- - os.execvpe
- - os.spawnl
- - os.spawnle
- - os.spawnlp
- - os.spawnlpe
- - os.spawnv
- - os.spawnve
- - os.spawnvp
- - os.spawnvpe
- - os.startfile
- :Example:
- .. code-block:: none
- >> Issue: [start_process_with_no_shell] Starting a process without a
- shell.
- Severity: Low Confidence: Medium
- CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
- Location: examples/os-spawn.py:8
- 7 os.spawnv(mode, path, args)
- 8 os.spawnve(mode, path, args, env)
- 9 os.spawnvp(mode, file, args)
- .. seealso::
- - https://security.openstack.org
- - https://docs.python.org/3/library/os.html#os.system
- - https://docs.python.org/3/library/subprocess.html#frequently-used-arguments
- - https://security.openstack.org/guidelines/dg_use-subprocess-securely.html
- - https://cwe.mitre.org/data/definitions/78.html
- .. versionadded:: 0.10.0
- .. versionchanged:: 1.7.3
- CWE information added
- """ # noqa: E501
- if config and context.call_function_name_qual in config["no_shell"]:
- return bandit.Issue(
- severity=bandit.LOW,
- confidence=bandit.MEDIUM,
- cwe=issue.Cwe.OS_COMMAND_INJECTION,
- text="Starting a process without a shell.",
- )
- @test.takes_config("shell_injection")
- @test.checks("Call")
- @test.test_id("B607")
- def start_process_with_partial_path(context, config):
- """**B607: Test for starting a process with a partial path**
- Python possesses many mechanisms to invoke an external executable. If the
- desired executable path is not fully qualified relative to the filesystem
- root then this may present a potential security risk.
- In POSIX environments, the `PATH` environment variable is used to specify a
- set of standard locations that will be searched for the first matching
- named executable. While convenient, this behavior may allow a malicious
- actor to exert control over a system. If they are able to adjust the
- contents of the `PATH` variable, or manipulate the file system, then a
- bogus executable may be discovered in place of the desired one. This
- executable will be invoked with the user privileges of the Python process
- that spawned it, potentially a highly privileged user.
- This test will scan the parameters of all configured Python methods,
- looking for paths that do not start at the filesystem root, that is, do not
- have a leading '/' character.
- **Config Options:**
- This plugin test shares a configuration with others in the same family,
- namely `shell_injection`. This configuration is divided up into three
- sections, `subprocess`, `shell` and `no_shell`. They each list Python calls
- that spawn subprocesses, invoke commands within a shell, or invoke commands
- without a shell (by replacing the calling process) respectively.
- This test will scan parameters of all methods in all sections. Note that
- methods are fully qualified and de-aliased prior to checking.
- .. code-block:: yaml
- shell_injection:
- # Start a process using the subprocess module, or one of its
- wrappers.
- subprocess:
- - subprocess.Popen
- - subprocess.call
- # Start a process with a function vulnerable to shell injection.
- shell:
- - os.system
- - os.popen
- - popen2.Popen3
- - popen2.Popen4
- - commands.getoutput
- - commands.getstatusoutput
- # Start a process with a function that is not vulnerable to shell
- injection.
- no_shell:
- - os.execl
- - os.execle
- :Example:
- .. code-block:: none
- >> Issue: Starting a process with a partial executable path
- Severity: Low Confidence: High
- CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
- Location: ./examples/partial_path_process.py:3
- 2 from subprocess import Popen as pop
- 3 pop('gcc --version', shell=False)
- .. seealso::
- - https://security.openstack.org
- - https://docs.python.org/3/library/os.html#process-management
- - https://cwe.mitre.org/data/definitions/78.html
- .. versionadded:: 0.13.0
- .. versionchanged:: 1.7.3
- CWE information added
- """
- if config and len(context.call_args):
- if (
- context.call_function_name_qual in config["subprocess"]
- or context.call_function_name_qual in config["shell"]
- or context.call_function_name_qual in config["no_shell"]
- ):
- node = context.node.args[0]
- # some calls take an arg list, check the first part
- if isinstance(node, ast.List):
- node = node.elts[0]
- # make sure the param is a string literal and not a var name
- if isinstance(node, ast.Str) and not full_path_match.match(node.s):
- return bandit.Issue(
- severity=bandit.LOW,
- confidence=bandit.HIGH,
- cwe=issue.Cwe.OS_COMMAND_INJECTION,
- text="Starting a process with a partial executable path",
- )
|