python.lang.security.audit.dangerous-subprocess-use-tainted-env-args.dangerous-subprocess-use-tainted-env-args

profile photo of semgrepsemgrep
Author
unknown
Download Count*

Detected subprocess function '$FUNC' with user controlled data. A malicious actor could leverage this to perform command injection. You may consider using 'shlex.escape()'.

Run Locally

Run in CI

Defintion

rules:
  - id: dangerous-subprocess-use-tainted-env-args
    mode: taint
    options:
      symbolic_propagation: true
    pattern-sources:
      - patterns:
          - pattern-either:
              - patterns:
                  - pattern-either:
                      - pattern: os.environ
                      - pattern: os.environ.get('$FOO', ...)
                      - pattern: os.environb
                      - pattern: os.environb.get('$FOO', ...)
                      - pattern: os.getenv('$ANYTHING', ...)
                      - pattern: os.getenvb('$ANYTHING', ...)
              - patterns:
                  - pattern-either:
                      - patterns:
                          - pattern-either:
                              - pattern: sys.argv
                              - pattern: sys.orig_argv
                      - patterns:
                          - pattern-inside: |
                              $PARSER = argparse.ArgumentParser(...)
                              ...
                          - pattern-inside: |
                              $ARGS = $PARSER.parse_args()
                          - pattern: <... $ARGS ...>
                      - patterns:
                          - pattern-inside: |
                              $PARSER = optparse.OptionParser(...)
                              ...
                          - pattern-inside: |
                              $ARGS = $PARSER.parse_args()
                          - pattern: <... $ARGS ...>
                      - patterns:
                          - pattern-either:
                              - pattern-inside: |
                                  $OPTS, $ARGS = getopt.getopt(...)
                                  ...
                              - pattern-inside: |
                                  $OPTS, $ARGS = getopt.gnu_getopt(...)
                                  ...
                          - pattern-either:
                              - patterns:
                                  - pattern-inside: |
                                      for $O, $A in $OPTS:
                                        ...
                                  - pattern: $A
                              - pattern: $ARGS
    pattern-sinks:
      - patterns:
          - pattern-either:
              - patterns:
                  - pattern-not: subprocess.$FUNC("...", ...)
                  - pattern-not: subprocess.$FUNC(["...",...], ...)
                  - pattern-not: subprocess.$FUNC(("...",...), ...)
                  - pattern-not: subprocess.CalledProcessError(...)
                  - pattern-not: subprocess.SubprocessError(...)
                  - pattern: subprocess.$FUNC($CMD, ...)
              - patterns:
                  - pattern-not: subprocess.$FUNC("=~/(sh|bash|ksh|csh|tcsh|zsh)/","-c","...",...)
                  - pattern: subprocess.$FUNC("=~/(sh|bash|ksh|csh|tcsh|zsh)/","-c", $CMD)
              - patterns:
                  - pattern-not: subprocess.$FUNC(["=~/(sh|bash|ksh|csh|tcsh|zsh)/","-c","...",...],...)
                  - pattern-not: subprocess.$FUNC(("=~/(sh|bash|ksh|csh|tcsh|zsh)/","-c","...",...),...)
                  - pattern-either:
                      - pattern: subprocess.$FUNC(["=~/(sh|bash|ksh|csh|tcsh|zsh)/","-c", $CMD], ...)
                      - pattern: subprocess.$FUNC(("=~/(sh|bash|ksh|csh|tcsh|zsh)/","-c", $CMD), ...)
              - patterns:
                  - pattern-not: subprocess.$FUNC("=~/(python)/","...",...)
                  - pattern: subprocess.$FUNC("=~/(python)/", $CMD)
              - patterns:
                  - pattern-not: subprocess.$FUNC(["=~/(python)/","...",...],...)
                  - pattern-not: subprocess.$FUNC(("=~/(python)/","...",...),...)
                  - pattern-either:
                      - pattern: subprocess.$FUNC(["=~/(python)/", $CMD],...)
                      - pattern: subprocess.$FUNC(("=~/(python)/", $CMD),...)
          - focus-metavariable: $CMD
    message: Detected subprocess function '$FUNC' with user controlled data. A
      malicious actor could leverage this to perform command injection. You may
      consider using 'shlex.escape()'.
    metadata:
      owasp:
        - A01:2017 - Injection
        - A03:2021 - Injection
      cwe:
        - "CWE-78: Improper Neutralization of Special Elements used in an OS
          Command ('OS Command Injection')"
      asvs:
        section: "V5: Validation, Sanitization and Encoding Verification Requirements"
        control_id: 5.3.8 OS Command Injection
        control_url: https://github.com/OWASP/ASVS/blob/master/4.0/en/0x13-V5-Validation-Sanitization-Encoding.md#v53-output-encoding-and-injection-prevention-requirements
        version: "4"
      references:
        - https://stackoverflow.com/questions/3172470/actual-meaning-of-shell-true-in-subprocess
        - https://docs.python.org/3/library/subprocess.html
        - https://docs.python.org/3/library/shlex.html
        - https://semgrep.dev/docs/cheat-sheets/python-command-injection/
      category: security
      technology:
        - python
      confidence: MEDIUM
      cwe2022-top25: true
      cwe2021-top25: true
      subcategory:
        - vuln
      likelihood: MEDIUM
      impact: MEDIUM
      license: Commons Clause License Condition v1.0[LGPL-2.1-only]
      vulnerability_class:
        - Command Injection
    languages:
      - python
    severity: ERROR

Examples

dangerous-subprocess-use-tainted-env-args.py

# cf. https://github.com/returntocorp/semgrep/blob/develop/docs/writing_rules/examples.md#auditing-dangerous-function-use-tainted-env-args

import subprocess
import sys


def ok():
    # ok:dangerous-subprocess-use-tainted-env-args
    subprocess.call("echo 'hello'")

    # ok:dangerous-subprocess-use-tainted-env-args
    subprocess.call(["echo", "a", ";", "rm", "-rf", "/"])

    # ok:dangerous-subprocess-use-tainted-env-args
    subprocess.call(("echo", "a", ";", "rm", "-rf", "/"))

    # ok:dangerous-subprocess-use-tainted-env-args
    raise subprocess.CalledProcessError("{}".format("foo"))

    # ok:dangerous-subprocess-use-tainted-env-args
    raise subprocess.SubprocessError("{}".format("foo"))


def bad1():
    cmd = sys.argv[1]
    # ruleid:dangerous-subprocess-use-tainted-env-args
    subprocess.call(cmd)


def bad2():
    # ruleid:dangerous-subprocess-use-tainted-env-args
    subprocess.call("grep -R {} .".format(sys.argv[1]))


def bad3():
    # ruleid:dangerous-subprocess-use-tainted-env-args
    subprocess.call("grep -R {} .".format(sys.argv[1]), shell=True)


def bad4():
    # ruleid:dangerous-subprocess-use-tainted-env-args
    subprocess.call("grep -R {} .".format(sys.argv[1]), shell=True, cwd="/home/user")


def bad5():
    # ruleid:dangerous-subprocess-use-tainted-env-args
    subprocess.run("grep -R {} .".format(sys.argv[1]), shell=True)


def bad6():
    # ruleid:dangerous-subprocess-use-tainted-env-args
    subprocess.run(["bash", "-c", sys.argv[1]], shell=True)


def bad7():
    cmd = sys.argv[1]
    # ruleid:dangerous-subprocess-use-tainted-env-args
    subprocess.call([cmd[0], cmd[1], "some", "args"])


def fn1(user_input):
    cmd = user_input.split()
    # fn:dangerous-subprocess-use-tainted-env-args
    subprocess.call([cmd[0], cmd[1], "some", "args"])


def fn2(payload: str) -> None:
    with tempfile.TemporaryDirectory() as directory:
        python_file = Path(directory) / "hello_world.py"
        python_file.write_text(
            textwrap.dedent(
                """
        print("What is your name?")
        name = input()
        print("Hello " + name)
    """
            )
        )
        # fn:dangerous-subprocess-use-tainted-env-args
        program = subprocess.Popen(
            ["python2", str(python_file)], stdin=subprocess.PIPE, text=True
        )
        program.communicate(input=payload, timeout=1)