Repository: privacyidea/privacyidea

Issue:
subprocess call - check for execution of untrusted input.
Check if there is possible user input to the openssl commands

Tracking issue for:
- [x] https://github.com/privacyidea/privacyidea/security/code-scanning/11
- [x] https://github.com/privacyidea/privacyidea/security/code-scanning/10
- [x] https://github.com/privacyidea/privacyidea/security/code-scanning/9
- [x] https://github.com/privacyidea/privacyidea/security/code-scanning/8
- [x] https://github.com/privacyidea/privacyidea/security/code-scanning/7
- [x] https://github.com/privacyidea/privacyidea/security/code-scanning/6



Discussion:
@jona-samuel If these can all be dismissed, please add the `#nosec` comments with the corresping B-number.
Then you may assign the review to @cornelinux 

Resolving patch:
diff --git a/privacyidea/lib/caconnectors/localca.py b/privacyidea/lib/caconnectors/localca.py
--- a/privacyidea/lib/caconnectors/localca.py
+++ b/privacyidea/lib/caconnectors/localca.py
@@ -31,7 +31,7 @@
 from privacyidea.lib.utils import int_to_hex, to_unicode
 from privacyidea.lib.caconnectors.baseca import BaseCAConnector, AvailableCAConnectors
 from OpenSSL import crypto
-from subprocess import Popen, PIPE
+from subprocess import Popen, PIPE  # nosec B404
 import yaml
 import datetime
 import shlex
@@ -424,7 +424,8 @@ def sign_request(self, csr, options=None):
                                                           certificate_filename))
         # run the command
         args = shlex.split(cmd)
-        p = Popen(args, stdout=PIPE, stderr=PIPE, cwd=workingdir, universal_newlines=True)
+        # the command is configured by the administrator: CA key, CA cert, number of days, the config file
+        p = Popen(args, stdout=PIPE, stderr=PIPE, cwd=workingdir, universal_newlines=True)  # nosec B603
         result, error = p.communicate()
         if p.returncode != 0:  # pragma: no cover
             # Some error occurred
@@ -479,6 +480,8 @@ def revoke_cert(self, certificate, request_id=None, reason=CRL_REASONS[0]):
             cert_obj = certificate
         else:
             raise CAError("Certificate in unsupported format")
+        if reason not in CRL_REASONS:
+            raise CAError("Unsupported revoke reason")
 
         serial = cert_obj.get_serial_number()
         serial_hex = int_to_hex(serial)
@@ -489,7 +492,9 @@ def revoke_cert(self, certificate, request_id=None, reason=CRL_REASONS[0]):
                                reason=reason)
         workingdir = self.config.get(ATTR.WORKING_DIR)
         args = shlex.split(cmd)
-        p = Popen(args, stdout=PIPE, stderr=PIPE, cwd=workingdir, universal_newlines=True)
+        # The command is configured by the administrator: CA key, CA cert, config file, certificate,
+        # the revoking reason is fetched earlier
+        p = Popen(args, stdout=PIPE, stderr=PIPE, cwd=workingdir, universal_newlines=True)  # nosec B603
         result, error = p.communicate()
         if p.returncode != 0:  # pragma: no cover
             # Some error occurred
@@ -534,7 +539,8 @@ def create_crl(self, publish=True, check_validity=False):
                                          config=self.config.get(ATTR.OPENSSL_CNF),
                                          CRL=crl)
             args = shlex.split(cmd)
-            p = Popen(args, stdout=PIPE, stderr=PIPE, cwd=workingdir, universal_newlines=True)
+            # The command is configured by the admin: CA key, CA cert, config file and CRL location
+            p = Popen(args, stdout=PIPE, stderr=PIPE, cwd=workingdir, universal_newlines=True)  # nosec B603
             result, error = p.communicate()
             if p.returncode != 0:  # pragma: no cover
                 # Some error occurred
@@ -683,7 +689,8 @@ def _init_ca(config):
     print("Running command...")
     print(command)
     args = shlex.split(command)
-    p = Popen(args, stdout=PIPE, stderr=PIPE, cwd=config.directory, universal_newlines=True)
+    # The command is created by the root user at the command line anyways
+    p = Popen(args, stdout=PIPE, stderr=PIPE, cwd=config.directory, universal_newlines=True)  # nosec B603
     result, error = p.communicate()
     if p.returncode != 0:  # pragma: no cover
         # Some error occurred
@@ -697,7 +704,8 @@ def _init_ca(config):
     print("Running command...")
     print(command)
     args = shlex.split(command)
-    p = Popen(args, stdout=PIPE, stderr=PIPE, cwd=config.directory, universal_newlines=True)
+    # The command is created by the root user at the command line anyways
+    p = Popen(args, stdout=PIPE, stderr=PIPE, cwd=config.directory, universal_newlines=True)  # nosec B603
     result, error = p.communicate()
     if p.returncode != 0:  # pragma: no cover
         # Some error occurred
diff --git a/privacyidea/lib/smsprovider/ScriptSMSProvider.py b/privacyidea/lib/smsprovider/ScriptSMSProvider.py
--- a/privacyidea/lib/smsprovider/ScriptSMSProvider.py
+++ b/privacyidea/lib/smsprovider/ScriptSMSProvider.py
@@ -78,8 +78,8 @@ def submit_message(self, phone, message):
         try:
             log.info("Starting script {script!r}.".format(script=script_name))
             # Trusted input/no user input: The scripts are created by user root and read from hard disk
-            p = subprocess.Popen(proc_args, cwd=self.script_directory,
-                                 universal_newlines=True, stdin=subprocess.PIPE)  # nosec B603
+            p = subprocess.Popen(proc_args, cwd=self.script_directory,   # nosec B603
+                                 universal_newlines=True, stdin=subprocess.PIPE)
             p.communicate(message)
             if background == SCRIPT_WAIT:
                 rcode = p.wait()


Test patch:
diff --git a/tests/test_lib_caconnector.py b/tests/test_lib_caconnector.py
--- a/tests/test_lib_caconnector.py
+++ b/tests/test_lib_caconnector.py
@@ -287,6 +287,9 @@ def test_02_sign_cert(self):
                          "'/C=DE/ST=Hessen/O=privacyidea/CN=requester"
                          ".localdomain'>")
 
+        # Fail to revoke certificate due to non-existing-reasing
+        self.assertRaises(CAError, cacon.revoke_cert, cert, reason="$(rm -fr)")
+
         # Revoke certificate
         r = cacon.revoke_cert(cert)
         serial_hex = int_to_hex(serial)