Code Style Checker: continued to implement checker for security related issues.

Tue, 09 Jun 2020 20:10:59 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Tue, 09 Jun 2020 20:10:59 +0200
changeset 7614
646742c260bd
parent 7613
382f89c11e27
child 7615
ca2949b1a29a

Code Style Checker: continued to implement checker for security related issues.

eric6.e4p file | annotate | diff | comparison | revisions
eric6/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleCheckerDialog.py file | annotate | diff | comparison | revisions
eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/assert.py file | annotate | diff | comparison | revisions
eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/djangoXssVulnerability.py file | annotate | diff | comparison | revisions
eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/exec.py file | annotate | diff | comparison | revisions
eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/generalBindAllInterfaces.py file | annotate | diff | comparison | revisions
eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/generalFilePermissions.py file | annotate | diff | comparison | revisions
eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/generalHardcodedPassword.py file | annotate | diff | comparison | revisions
eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/generalHardcodedTmp.py file | annotate | diff | comparison | revisions
eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/injectionParamiko.py file | annotate | diff | comparison | revisions
eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/injectionShell.py file | annotate | diff | comparison | revisions
eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/insecureHashlibNew.py file | annotate | diff | comparison | revisions
eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/SecurityChecker.py file | annotate | diff | comparison | revisions
eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/SecurityContext.py file | annotate | diff | comparison | revisions
eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/SecurityDefaults.py file | annotate | diff | comparison | revisions
eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/translations.py file | annotate | diff | comparison | revisions
--- a/eric6.e4p	Mon Jun 08 20:08:27 2020 +0200
+++ b/eric6.e4p	Tue Jun 09 20:10:59 2020 +0200
@@ -326,10 +326,19 @@
     <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/certificateValidation.py</Source>
     <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/djangoSqlInjection.py</Source>
     <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/djangoXssVulnerability.py</Source>
+    <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/exec.py</Source>
     <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/flaskDebug.py</Source>
+    <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/generalBindAllInterfaces.py</Source>
+    <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/generalFilePermissions.py</Source>
+    <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/generalHardcodedPassword.py</Source>
+    <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/generalHardcodedTmp.py</Source>
+    <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/injectionParamiko.py</Source>
+    <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/injectionShell.py</Source>
+    <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/insecureHashlibNew.py</Source>
     <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/yamlLoad.py</Source>
     <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/SecurityChecker.py</Source>
     <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/SecurityContext.py</Source>
+    <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/SecurityDefaults.py</Source>
     <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/SecurityNodeVisitor.py</Source>
     <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/SecurityUtils.py</Source>
     <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/__init__.py</Source>
--- a/eric6/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleCheckerDialog.py	Mon Jun 08 20:08:27 2020 +0200
+++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleCheckerDialog.py	Tue Jun 09 20:10:59 2020 +0200
@@ -248,9 +248,12 @@
         
         fixable = False
         itm = QTreeWidgetItem(
-            self.__lastFileItem,
-            ["{0:6}".format(result["line"]), result["code"],
-             result["display"]])
+            self.__lastFileItem, [
+                "{0:6}".format(result["line"]),
+                result["code"],
+                result["display"]
+            ]
+        )
         if result["code"].startswith(("W", "-", "C", "M")):
             itm.setIcon(1, UI.PixmapCache.getIcon("warning"))
         elif result["code"].startswith(("A", "N")):
@@ -293,7 +296,7 @@
         itm.setData(0, self.positionRole, int(result["offset"]))
         itm.setData(0, self.messageRole, result["display"])
         itm.setData(0, self.fixableRole, fixable)
-        itm.setData(0, self.codeRole, result["code"])
+        itm.setData(0, self.codeRole, result["code"].split(".", 1)[0])
         itm.setData(0, self.ignoredRole, result["ignored"])
         itm.setData(0, self.argsRole, result["args"])
         
@@ -448,6 +451,8 @@
                 "MaximumComplexity": 3,
             }
         
+        # TODO: add 'SecurityChecker'
+        
         self.__initCategoriesList(self.__data["EnabledCheckerCategories"])
         self.excludeFilesEdit.setText(self.__data["ExcludeFiles"])
         self.excludeMessagesEdit.setText(self.__data["ExcludeMessages"])
@@ -596,7 +601,7 @@
                     self.maxAnnotationsComplexitySpinBox.value(),
             }
             
-            # TODO: implement safety arguments
+            # TODO: add 'SecurityChecker'
             safetyArgs = {}
             
             self.__options = [excludeMessages, includeMessages, repeatMessages,
@@ -944,6 +949,8 @@
                     "MaximumComplexity":
                         self.maxAnnotationsComplexitySpinBox.value(),
                 }
+                
+                # TODO: add 'SecurityChecker'
             }
             if data != self.__data:
                 self.__data = data
@@ -1041,6 +1048,7 @@
             vm.openSourceFile(fn, lineno=lineno, pos=position + 1)
             editor = vm.getOpenEditor(fn)
             
+            # TODO: add other syntax errors or do syntax check once for all
             if code in ["E901", "E902"]:
                 editor.toggleSyntaxError(lineno, 0, True, message, True)
             else:
@@ -1177,6 +1185,8 @@
             Preferences.Prefs.settings.value(
                 "PEP8/MaximumAnnotationComplexity", 3)))
         
+        # TODO: add 'SecurityChecker'
+        
         self.__cleanupData()
     
     @pyqtSlot()
@@ -1243,6 +1253,8 @@
         Preferences.Prefs.settings.setValue(
             "PEP8/MaximumAnnotationComplexity",
             self.maxAnnotationsComplexitySpinBox.value())
+        
+        # TODO: add 'SecurityChecker'
     
     @pyqtSlot()
     def on_resetDefaultButton_clicked(self):
@@ -1290,6 +1302,8 @@
         Preferences.Prefs.settings.setValue(
             "PEP8/MaximumAnnotationComplexity", 3)
         
+        # TODO: add 'SecurityChecker'
+        
         # Update UI with default values
         self.on_loadDefaultButton_clicked()
     
--- a/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/assert.py	Mon Jun 08 20:08:27 2020 +0200
+++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/assert.py	Tue Jun 09 20:10:59 2020 +0200
@@ -43,7 +43,7 @@
     @type dict
     """
     reportError(
-        context.node.lineno,
+        context.node.lineno - 1,
         context.node.col_offset,
         "S101",
         "L",
--- a/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/djangoXssVulnerability.py	Mon Jun 08 20:08:27 2020 +0200
+++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/djangoXssVulnerability.py	Tue Jun 09 20:10:59 2020 +0200
@@ -18,7 +18,7 @@
 import ast
 import sys
 
-PY2 = sys.version_info < (3, 0, 0)
+PY2 = sys.version_info[0] == 2
 
 
 def getChecks():
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/exec.py	Tue Jun 09 20:10:59 2020 +0200
@@ -0,0 +1,64 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a check for the use of 'exec'.
+"""
+
+#
+# This is a modified version of the one found in the bandit package.
+#
+# Original Copyright 2014 Hewlett-Packard Development Company, L.P.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+import sys
+
+
+def getChecks():
+    """
+    Public method to get a dictionary with checks handled by this module.
+    
+    @return dictionary containing checker lists containing checker function and
+        list of codes
+    @rtype dict
+    """
+    if sys.version_info[0] == 2:
+        return {
+            "Exec": [
+                (checkExecUsed, ("S102",)),
+            ],
+        }
+    else:
+        return {
+            "Call": [
+                (checkExecUsed, ("S102",)),
+            ],
+        }
+
+
+def checkExecUsed(reportError, context, config):
+    """
+    Function to check for the use of 'exec'.
+    
+    @param reportError function to be used to report errors
+    @type func
+    @param context security context object
+    @type SecurityContext
+    @param config dictionary with configuration data
+    @type dict
+    """
+    if (
+        sys.version_info[0] == 2 or
+        context.callFunctionNameQual == 'exec'
+    ):
+        reportError(
+            context.node.lineno - 1,
+            context.node.col_offset,
+            "S102",
+            "M",
+            "H"
+        )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/generalBindAllInterfaces.py	Tue Jun 09 20:10:59 2020 +0200
@@ -0,0 +1,52 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a check for binding to all interfaces.
+"""
+
+#
+# This is a modified version of the one found in the bandit package.
+#
+# Original Copyright 2014 Hewlett-Packard Development Company, L.P.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+
+def getChecks():
+    """
+    Public method to get a dictionary with checks handled by this module.
+    
+    @return dictionary containing checker lists containing checker function and
+        list of codes
+    @rtype dict
+    """
+    return {
+        "Str": [
+            (checkBindAllInterfaces, ("S104",)),
+        ],
+    }
+
+
+def checkBindAllInterfaces(reportError, context, config):
+    """
+    Function to check for binding to all interfaces.
+    
+    @param reportError function to be used to report errors
+    @type func
+    @param context security context object
+    @type SecurityContext
+    @param config dictionary with configuration data
+    @type dict
+    """
+    if context.stringVal == '0.0.0.0':
+        reportError(
+            context.node.lineno - 1,
+            context.node.col_offset,
+            "S104",
+            "M",
+            "M",
+        )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/generalFilePermissions.py	Tue Jun 09 20:10:59 2020 +0200
@@ -0,0 +1,74 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a check for setting too permissive file permissions.
+"""
+
+#
+# This is a modified version of the one found in the bandit package.
+#
+# Original Copyright 2014 Hewlett-Packard Development Company, L.P.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+import stat
+
+
+def getChecks():
+    """
+    Public method to get a dictionary with checks handled by this module.
+    
+    @return dictionary containing checker lists containing checker function and
+        list of codes
+    @rtype dict
+    """
+    return {
+        "Call": [
+            (checkFilePermissions, ("S102",)),
+        ],
+    }
+
+
+def checkFilePermissions(reportError, context, config):
+    """
+    Function to check for setting too permissive file permissions.
+    
+    @param reportError function to be used to report errors
+    @type func
+    @param context security context object
+    @type SecurityContext
+    @param config dictionary with configuration data
+    @type dict
+    """
+    if 'chmod' in context.callFunctionName:
+        if context.callArgsCount == 2:
+            mode = context.getCallArgAtPosition(1)
+            
+            if (
+                mode is not None and
+                isinstance(mode, int) and
+                (mode & stat.S_IWOTH or mode & stat.S_IXGRP)
+            ):
+                # world writable is an HIGH, group executable is a MEDIUM
+                if mode & stat.S_IWOTH:
+                    severity = "H"
+                else:
+                    severity = "M"
+                
+                filename = context.getCallArgAtPosition(0)
+                if filename is None:
+                    filename = 'NOT PARSED'
+                
+                reportError(
+                    context.node.lineno - 1,
+                    context.node.col_offset,
+                    "S103",
+                    severity,
+                    "H",
+                    oct(mode),
+                    filename
+                )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/generalHardcodedPassword.py	Tue Jun 09 20:10:59 2020 +0200
@@ -0,0 +1,168 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing checks for potentially hardcoded passwords.
+"""
+
+#
+# This is a modified version of the one found in the bandit package.
+#
+# Original Copyright 2014 Hewlett-Packard Development Company, L.P.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+import ast
+import re
+import sys
+
+RE_WORDS = "(pas+wo?r?d|pass(phrase)?|pwd|token|secrete?|ken+wort|geheim)"
+RE_CANDIDATES = re.compile(
+    '(^{0}$|_{0}_|^{0}_|_{0}$)'.format(RE_WORDS),
+    re.IGNORECASE
+)
+
+
+def getChecks():
+    """
+    Public method to get a dictionary with checks handled by this module.
+    
+    @return dictionary containing checker lists containing checker function and
+        list of codes
+    @rtype dict
+    """
+    return {
+        "Str": [
+            (checkHardcodedPasswordAsString, ("S105",)),
+        ],
+        "Call": [
+            (checkHardcodedPasswordAsFunctionArg, ("S106",)),
+        ],
+        "FunctionDef": [
+            (checkHardcodedPasswordAsDefault, ("S107",)),
+        ],
+    }
+
+
+def checkHardcodedPasswordAsString(reportError, context, config):
+    """
+    Function to check for use of hardcoded password strings.
+    
+    @param reportError function to be used to report errors
+    @type func
+    @param context security context object
+    @type SecurityContext
+    @param config dictionary with configuration data
+    @type dict
+    """
+    node = context.node
+    if isinstance(node._securityParent, ast.Assign):
+        # looks for "candidate='some_string'"
+        for targ in node._securityParent.targets:
+            if isinstance(targ, ast.Name) and RE_CANDIDATES.search(targ.id):
+                reportError(
+                    context.node.lineno - 1,
+                    context.node.col_offset,
+                    "S105",
+                    "L",
+                    "M",
+                    node.s
+                )
+    
+    elif (
+        isinstance(node._securityParent, ast.Index) and
+        RE_CANDIDATES.search(node.s)
+    ):
+        # looks for "dict[candidate]='some_string'"
+        # assign -> subscript -> index -> string
+        assign = node._securityParent._securityParent._securityParent
+        if (
+            isinstance(assign, ast.Assign) and
+            isinstance(assign.value, ast.Str)
+        ):
+            reportError(
+                context.node.lineno - 1,
+                context.node.col_offset,
+                "S105",
+                "L",
+                "M",
+                assign.value.s
+            )
+    
+    elif isinstance(node._securityParent, ast.Compare):
+        # looks for "candidate == 'some_string'"
+        comp = node._securityParent
+        if isinstance(comp.left, ast.Name):
+            if RE_CANDIDATES.search(comp.left.id):
+                if isinstance(comp.comparators[0], ast.Str):
+                    reportError(
+                        context.node.lineno - 1,
+                        context.node.col_offset,
+                        "S105",
+                        "L",
+                        "M",
+                        comp.comparators[0].s
+                    )
+
+
+def checkHardcodedPasswordAsFunctionArg(reportError, context, config):
+    """
+    Function to check for use of hard-coded password function arguments.
+    
+    @param reportError function to be used to report errors
+    @type func
+    @param context security context object
+    @type SecurityContext
+    @param config dictionary with configuration data
+    @type dict
+    """
+    # looks for "function(candidate='some_string')"
+    for kw in context.node.keywords:
+        if isinstance(kw.value, ast.Str) and RE_CANDIDATES.search(kw.arg):
+            reportError(
+                context.node.lineno - 1,
+                context.node.col_offset,
+                "S106",
+                "L",
+                "M",
+                kw.value.s
+            )
+
+
+def checkHardcodedPasswordAsDefault(reportError, context, config):
+    """
+    Function to check for use of hard-coded password argument defaults.
+    
+    @param reportError function to be used to report errors
+    @type func
+    @param context security context object
+    @type SecurityContext
+    @param config dictionary with configuration data
+    @type dict
+    """
+    # looks for "def function(candidate='some_string')"
+    
+    # this pads the list of default values with "None" if nothing is given
+    defs = [None] * (len(context.node.args.args) -
+                     len(context.node.args.defaults))
+    defs.extend(context.node.args.defaults)
+    
+    # go through all (param, value)s and look for candidates
+    for key, val in zip(context.node.args.args, defs):
+        isPy3Arg = True
+        if sys.version_info[0] > 2:
+            isPy3Arg = isinstance(key, ast.arg)
+        if isinstance(key, ast.Name) or isPy3Arg:
+            check = key.arg if sys.version_info[0] > 2 else key.id  # Py3
+            if isinstance(val, ast.Str) and RE_CANDIDATES.search(check):
+                reportError(
+                    context.node.lineno - 1,
+                    context.node.col_offset,
+                    "S107",
+                    "L",
+                    "M",
+                    val.s
+                )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/generalHardcodedTmp.py	Tue Jun 09 20:10:59 2020 +0200
@@ -0,0 +1,57 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a check for insecure usage of tmp file/directory.
+"""
+
+#
+# This is a modified version of the one found in the bandit package.
+#
+# Original Copyright 2014 Hewlett-Packard Development Company, L.P.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+
+def getChecks():
+    """
+    Public method to get a dictionary with checks handled by this module.
+    
+    @return dictionary containing checker lists containing checker function and
+        list of codes
+    @rtype dict
+    """
+    return {
+        "Str": [
+            (checkHardcodedTmpDirectory, ("S108",)),
+        ],
+    }
+
+
+def checkHardcodedTmpDirectory(reportError, context, config):
+    """
+    Function to check for insecure usage of tmp file/directory.
+    
+    @param reportError function to be used to report errors
+    @type func
+    @param context security context object
+    @type SecurityContext
+    @param config dictionary with configuration data
+    @type dict
+    """
+    if config and "hardcoded_tmp_directories" in config:
+        tmpDirs = config["hardcoded_tmp_directories"]
+    else:
+        tmpDirs = ["/tmp", "/var/tmp", "/dev/shm", "~/tmp"]
+    
+    if any(context.stringVal.startswith(s) for s in tmpDirs):
+        reportError(
+            context.node.lineno - 1,
+            context.node.col_offset,
+            "S108",
+            "M",
+            "M",
+        )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/injectionParamiko.py	Tue Jun 09 20:10:59 2020 +0200
@@ -0,0 +1,54 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a check for shell injection within Paramiko.
+"""
+
+#
+# This is a modified version of the one found in the bandit package.
+#
+# Original Copyright 2014 Hewlett-Packard Development Company, L.P.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+
+def getChecks():
+    """
+    Public method to get a dictionary with checks handled by this module.
+    
+    @return dictionary containing checker lists containing checker function and
+        list of codes
+    @rtype dict
+    """
+    return {
+        "Call": [
+            (checkParamikoCalls, ("S601",)),
+        ],
+    }
+
+
+def checkParamikoCalls(reportError, context, config):
+    """
+    Function to check for shell injection within Paramiko.
+    
+    @param reportError function to be used to report errors
+    @type func
+    @param context security context object
+    @type SecurityContext
+    @param config dictionary with configuration data
+    @type dict
+    """
+    for module in ['paramiko']:
+        if context.isModuleImportedLike(module):
+            if context.callFunctionName in ['exec_command']:
+                reportError(
+                    context.node.lineno - 1,
+                    context.node.col_offset,
+                    "S601",
+                    "M",
+                    "M",
+                )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/injectionShell.py	Tue Jun 09 20:10:59 2020 +0200
@@ -0,0 +1,356 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a check for shell injection.
+"""
+
+#
+# This is a modified version of the one found in the bandit package.
+#
+# Original Copyright 2014 Hewlett-Packard Development Company, L.P.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+import ast
+import re
+import sys
+
+# This regex starts with a windows drive letter (eg C:)
+# or one of our path delimeter characters (/, \, .)
+fullPathMatchRe = re.compile(r'^(?:[A-Za-z](?=\:)|[\\\/\.])')
+
+
+def getChecks():
+    """
+    Public method to get a dictionary with checks handled by this module.
+    
+    @return dictionary containing checker lists containing checker function and
+        list of codes
+    @rtype dict
+    """
+    return {
+        "Call": [
+            (checkSubprocessPopenWithShell, ("S602",)),
+            (checkSubprocessPopenWithoutShell, ("S603",)),
+            (checkOtherFunctionWithShell, ("S604",)),
+            (checkStartProcessWithShell, ("S605",)),
+            (checkStartProcessWithNoShell, ("S606",)),
+            (checkStartProcessWithPartialPath, ("S607",)),
+        ],
+    }
+
+
+def _defaultValues(key):
+    """
+    Function to get the default values for a given check key.
+    
+    @param key key to get default values for
+    @type str
+    @return list with default values
+    @rtype list of str
+    """
+    if key == "shell_injection_subprocess":
+        return [
+            'subprocess.Popen',
+            'subprocess.call',
+            'subprocess.check_call',
+            'subprocess.check_output',
+            'subprocess.run'
+        ]
+    elif key == "shell_injection_shell":
+        return [
+            'os.system',
+            'os.popen',
+            'os.popen2',
+            'os.popen3',
+            'os.popen4',
+            'popen2.popen2',
+            'popen2.popen3',
+            'popen2.popen4',
+            'popen2.Popen3',
+            'popen2.Popen4',
+            'commands.getoutput',
+            'commands.getstatusoutput'
+        ]
+    elif key == "shell_injection_noshell":
+        return [
+            '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'
+        ]
+    else:
+        return []
+
+
+def _evaluateShellCall(context):
+    """
+    Function to determine the severity of a shell call.
+    
+    @param context context to be inspected
+    @type SecurityContext
+    @return severity level (L, M or H)
+    @rtype str
+    """
+    noFormatting = isinstance(context.node.args[0], ast.Str)
+
+    if noFormatting:
+        return "L"
+    else:
+        return "H"
+
+
+def hasShell(context):
+    """
+    Function to check, if the node of the context contains the shell keyword.
+    
+    @param context context to be inspected
+    @type SecurityContext
+    @return tuple containing a flag indicating the presence of the 'shell'
+        argument and flag indicating the value of the 'shell' argument
+    @rtype tuple of (bool, bool)
+    """
+    keywords = context.node.keywords
+    result = False
+    shell = False
+    if 'shell' in context.callKeywords:
+        shell = True
+        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 (
+                    sys.version_info[0] > 2 and
+                    isinstance(val, ast.NameConstant)
+                ):
+                    result = val.value
+                else:
+                    result = True
+    
+    return shell, result
+
+
+def checkSubprocessPopenWithShell(reportError, context, config):
+    """
+    Function to check for use of popen with shell equals true.
+    
+    @param reportError function to be used to report errors
+    @type func
+    @param context security context object
+    @type SecurityContext
+    @param config dictionary with configuration data
+    @type dict
+    """
+    if config and "shell_injection_subprocess" in config:
+        functionNames = config["shell_injection_subprocess"]
+    else:
+        functionNames = _defaultValues("shell_injection_subprocess")
+    
+    if context.callFunctionNameQual in functionNames:
+        shell, shellValue = hasShell(context)
+        if shell and shellValue:
+            if len(context.callArgs) > 0:
+                sev = _evaluateShellCall(context)
+                if sev == "L":
+                    reportError(
+                        context.getLinenoForCallArg('shell') - 1,
+                        context.getOffsetForCallArg('shell'),
+                        "S602.L",
+                        sev,
+                        "H",
+                    )
+                else:
+                    reportError(
+                        context.getLinenoForCallArg('shell') - 1,
+                        context.getOffsetForCallArg('shell'),
+                        "S602.H",
+                        sev,
+                        "H",
+                    )
+
+
+def checkSubprocessPopenWithoutShell(reportError, context, config):
+    """
+    Function to check for use of popen without shell equals true.
+    
+    @param reportError function to be used to report errors
+    @type func
+    @param context security context object
+    @type SecurityContext
+    @param config dictionary with configuration data
+    @type dict
+    """
+    if config and "shell_injection_subprocess" in config:
+        functionNames = config["shell_injection_subprocess"]
+    else:
+        functionNames = _defaultValues("shell_injection_subprocess")
+    
+    if context.callFunctionNameQual in functionNames:
+        if not hasShell(context)[0]:
+            reportError(
+                context.node.lineno - 1,
+                context.node.col_offset,
+                "S603",
+                "L",
+                "H",
+            )
+
+
+def checkOtherFunctionWithShell(reportError, context, config):
+    """
+    Function to check for any function with shell equals true.
+    
+    @param reportError function to be used to report errors
+    @type func
+    @param context security context object
+    @type SecurityContext
+    @param config dictionary with configuration data
+    @type dict
+    """
+    if config and "shell_injection_subprocess" in config:
+        functionNames = config["shell_injection_subprocess"]
+    else:
+        functionNames = _defaultValues("shell_injection_subprocess")
+    
+    if context.callFunctionNameQual not in functionNames:
+        shell, shellValue = hasShell(context)
+        if shell and shellValue:
+            reportError(
+                context.getLinenoForCallArg('shell') - 1,
+                context.getOffsetForCallArg('shell'),
+                "S604",
+                "M",
+                "L",
+            )
+
+
+def checkStartProcessWithShell(reportError, context, config):
+    """
+    Function to check for starting a process with a shell.
+    
+    @param reportError function to be used to report errors
+    @type func
+    @param context security context object
+    @type SecurityContext
+    @param config dictionary with configuration data
+    @type dict
+    """
+    if config and "shell_injection_shell" in config:
+        functionNames = config["shell_injection_shell"]
+    else:
+        functionNames = _defaultValues("shell_injection_shell")
+    
+    if context.callFunctionNameQual in functionNames:
+        if len(context.callArgs) > 0:
+            sev = _evaluateShellCall(context)
+            if sev == "L":
+                reportError(
+                    context.node.lineno - 1,
+                    context.node.col_offset,
+                    "S605.L",
+                    sev,
+                    "H",
+                )
+            else:
+                reportError(
+                    context.node.lineno - 1,
+                    context.node.col_offset,
+                    "S605.H",
+                    sev,
+                    "H",
+                )
+
+
+def checkStartProcessWithNoShell(reportError, context, config):
+    """
+    Function to check for starting a process with no shell.
+    
+    @param reportError function to be used to report errors
+    @type func
+    @param context security context object
+    @type SecurityContext
+    @param config dictionary with configuration data
+    @type dict
+    """
+    if config and "shell_injection_noshell" in config:
+        functionNames = config["shell_injection_noshell"]
+    else:
+        functionNames = _defaultValues("shell_injection_noshell")
+    
+    if context.callFunctionNameQual in functionNames:
+        reportError(
+            context.node.lineno - 1,
+            context.node.col_offset,
+            "S606",
+            "L",
+            "M",
+        )
+
+
+def checkStartProcessWithPartialPath(reportError, context, config):
+    """
+    Function to check for starting a process with no shell.
+    
+    @param reportError function to be used to report errors
+    @type func
+    @param context security context object
+    @type SecurityContext
+    @param config dictionary with configuration data
+    @type dict
+    """
+    if config and "shell_injection_subprocess" in config:
+        functionNames = config["shell_injection_subprocess"]
+    else:
+        functionNames = _defaultValues("shell_injection_subprocess")
+    
+    if config and "shell_injection_shell" in config:
+        functionNames += config["shell_injection_shell"]
+    else:
+        functionNames += _defaultValues("shell_injection_shell")
+    
+    if config and "shell_injection_noshell" in config:
+        functionNames += config["shell_injection_noshell"]
+    else:
+        functionNames += _defaultValues("shell_injection_noshell")
+    
+    if len(context.callArgs):
+        if context.callFunctionNameQual in functionNames:
+            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 fullPathMatchRe.match(node.s):
+                reportError(
+                    context.node.lineno - 1,
+                    context.node.col_offset,
+                    "S607",
+                    "L",
+                    "H",
+                )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/insecureHashlibNew.py	Tue Jun 09 20:10:59 2020 +0200
@@ -0,0 +1,70 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a check for use of insecure md4, md5, or sha1 hash
+functions in hashlib.new().
+"""
+
+#
+# This is a modified version of the one found in the bandit package.
+#
+# Original Copyright 2014 Hewlett-Packard Development Company, L.P.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+
+def getChecks():
+    """
+    Public method to get a dictionary with checks handled by this module.
+    
+    @return dictionary containing checker lists containing checker function and
+        list of codes
+    @rtype dict
+    """
+    return {
+        "Call": [
+            (checkHashlibNew, ("S324",)),
+        ],
+    }
+
+
+def checkHashlibNew(reportError, context, config):
+    """
+    Function to check for use of insecure md4, md5, or sha1 hash functions
+    in hashlib.new().
+    
+    @param reportError function to be used to report errors
+    @type func
+    @param context security context object
+    @type SecurityContext
+    @param config dictionary with configuration data
+    @type dict
+    """
+    if config and "insecure_hashes" in config:
+        insecureHashes = [h.lower() for h in config["insecure_hashes"]]
+    else:
+        insecureHashes = ['md4', 'md5', 'sha', 'sha1']
+    
+    if isinstance(context.callFunctionNameQual, str):
+        qualnameList = context.callFunctionNameQual.split('.')
+        func = qualnameList[-1]
+        if 'hashlib' in qualnameList and func == 'new':
+            args = context.callArgs
+            keywords = context.callKeywords
+            name = args[0] if args else keywords['name']
+            if (
+                isinstance(name, str) and
+                name.lower() in insecureHashes
+            ):
+                reportError(
+                    context.node.lineno - 1,
+                    context.node.col_offset,
+                    "S324",
+                    "M",
+                    "H",
+                    name.upper()
+                )
--- a/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/SecurityChecker.py	Mon Jun 08 20:08:27 2020 +0200
+++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/SecurityChecker.py	Tue Jun 09 20:10:59 2020 +0200
@@ -23,13 +23,30 @@
         # assert used
         "S101",
         
+        # exec used
+        "S102",
+        
+        # bad file permissions
+        "S103",
+        
+        # bind to all interfaces
+        "S104",
+        
+        # hardcoded passwords
+        "S105", "S106", "S107"
+        
+        # hardcoded tmp directory
+        "S108",
+        
         # flask app
         "S201",
         
         # insecure function calls (blacklisted)
         "S301", "S302", "S303", "S304", "S305", "S306", "S307", "S308", "S309",
         "S310", "S311", "S312", "S313", "S314", "S315", "S316", "S317", "S318",
-        "S319", "S320", "S321", "S322", "S323", "S325",
+        "S319", "S320", "S321", "S322", "S323", "S325",     # TODO: check S324
+        # hashlib.new
+        "S324",
         
         # insecure imports (blacklisted)
         "S401", "S402", "S403", "S404", "S405", "S406", "S407", "S408", "S409",
@@ -38,14 +55,17 @@
         # insecure certificate usage
         "S501",
         
+        # YAML load
+        "S506",
+        
+        # Shell injection
+        "S601", "S602", "S603", "S604", "S605", "S606", "S607",
+        
         # Django SQL injection
         "S610", "S611",
         
         # Django XSS vulnerability
         "S703",
-        
-        # YAML load
-        "S506",
     ]
     
     def __init__(self, source, filename, select, ignore, expected, repeat,
@@ -65,7 +85,7 @@
         @type list of str
         @param repeat flag indicating to report each occurrence of a code
         @type bool
-        @param args dictionary of arguments for the miscellaneous checks
+        @param args dictionary of arguments for the security checks
         @type dict
         """
         self.__select = tuple(select)
@@ -158,12 +178,12 @@
                 offset = offset[1:3]
         else:
             offset = (1, 0)
-        self.__error(offset[0] - 1,
-                     offset[1] or 0,
-                     'S999',
-                     "H",
-                     "H",
-                     exc_type.__name__, exc.args[0])
+        self.reportError(offset[0] - 1,
+                         offset[1] or 0,
+                         'S999',
+                         "H",
+                         "H",
+                         exc_type.__name__, exc.args[0])
     
     def __generateTree(self):
         """
--- a/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/SecurityContext.py	Mon Jun 08 20:08:27 2020 +0200
+++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/SecurityContext.py	Tue Jun 09 20:10:59 2020 +0200
@@ -266,14 +266,14 @@
         # tend to refer to things like True and False. This prevents them from
         # being re-assigned in Python 3.
         elif (
-            sys.version_info >= (3, 0, 0) and
+            sys.version_info[0] >= 3 and
             isinstance(literal, ast.NameConstant)
         ):
             literalValue = str(literal.value)
         
         # Bytes are only part of the AST in Python 3
         elif (
-            sys.version_info >= (3, 0, 0) and
+            sys.version_info[0] >= 3 and
             isinstance(literal, ast.Bytes)
         ):
             literalValue = literal.s
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/SecurityDefaults.py	Tue Jun 09 20:10:59 2020 +0200
@@ -0,0 +1,50 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing the default values for some check modules.
+"""
+
+SecurityDefaults = {
+    "hardcoded_tmp_directories": ["/tmp", "/var/tmp", "/dev/shm", "~/tmp"],
+    "insecure_hashes": ['md4', 'md5', 'sha', 'sha1'],
+    "shell_injection_subprocess": [
+        'subprocess.Popen',
+        'subprocess.call',
+        'subprocess.check_call',
+        'subprocess.check_output',
+        'subprocess.run'],
+    "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'],
+    "shell_injection_noshell": [
+        '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'],
+}
--- a/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/translations.py	Mon Jun 08 20:08:27 2020 +0200
+++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/translations.py	Tue Jun 09 20:10:59 2020 +0200
@@ -15,9 +15,40 @@
     # assert used
     "S101": QCoreApplication.translate(
         "Security",
-        "Use of assert detected. The enclosed code will be removed when"
+        "Use of 'assert' detected. The enclosed code will be removed when"
         " compiling to optimised byte code."),
     
+    # exec used
+    "S102": QCoreApplication.translate(
+        "Security",
+        "Use of 'exec' detected."),
+    
+    # bad file permissions
+    "S103": QCoreApplication.translate(
+        "Security",
+        "'chmod' setting a permissive mask {0} on file ({1})."),
+    
+    # bind to all interfaces
+    "S104": QCoreApplication.translate(
+        "Security",
+        "Possible binding to all interfaces."),
+    
+    # hardcoded passwords
+    "S105": QCoreApplication.translate(
+        "Security",
+        "Possible hardcoded password: '{0}'"),
+    "S106": QCoreApplication.translate(
+        "Security",
+        "Possible hardcoded password: '{0}'"),
+    "S107": QCoreApplication.translate(
+        "Security",
+        "Possible hardcoded password: '{0}'"),
+    
+    # hardcoded tmp directory
+    "S108": QCoreApplication.translate(
+        "Security",
+        "Probable insecure usage of temp file/directory."),
+    
     # flask app
     "S201": QCoreApplication.translate(
         "Security",
@@ -132,6 +163,11 @@
         "Use of os.tempnam() and os.tmpnam() is vulnerable to symlink"
         " attacks. Consider using tmpfile() instead."),
     
+    # hashlib.new
+    "S324": QCoreApplication.translate(
+        "Security",
+        "Use of insecure {0} hash function."),
+    
     # blacklisted imports
     "S401": QCoreApplication.translate(
         "Security",
@@ -197,27 +233,65 @@
     # insecure certificate usage
     "S501": QCoreApplication.translate(
         "Security",
-        "Requests call with verify=False disabling SSL certificate checks,"
+        "'requests' call with verify=False disabling SSL certificate checks,"
         " security issue."),
     
     # YAML load
     "S506": QCoreApplication.translate(
         "Security",
-        "Use of unsafe yaml load. Allows instantiation of arbitrary objects."
-        " Consider yaml.safe_load()."),
+        "Use of unsafe 'yaml.load()'. Allows instantiation of arbitrary"
+        " objects. Consider 'yaml.safe_load()'."),
+    
+    # Shell injection
+    "S601": QCoreApplication.translate(
+        "Security",
+        "Possible shell injection via 'Paramiko' call, check inputs are"
+        " properly sanitized."),
+    "S602.L": QCoreApplication.translate(
+        "Security",
+        "'subprocess' call with shell=True seems safe, but may be changed"
+        " in the future, consider rewriting without shell"),
+    "S602.H": QCoreApplication.translate(
+        "Security",
+        "'subprocess' call with shell=True identified, security issue."),
+    "S603": QCoreApplication.translate(
+        "Security",
+        "'subprocess' call - check for execution of untrusted input."),
+    "S604": QCoreApplication.translate(
+        "Security",
+        "Function call with shell=True parameter identified, possible"
+        " security issue."),
+    "S605.L": QCoreApplication.translate(
+        "Security",
+        "Starting a process with a shell: Seems safe, but may be changed in"
+        " the future, consider rewriting without shell"),
+    "S605.H": QCoreApplication.translate(
+        "Security",
+        "Starting a process with a shell, possible injection detected,"
+        " security issue."),
+    "S606": QCoreApplication.translate(
+        "Security",
+        "Starting a process without a shell."),
+    "S607": QCoreApplication.translate(
+        "Security",
+        "Starting a process with a partial executable path."),
     
     # Django SQL injection
     "S610": QCoreApplication.translate(
         "Security",
-        "Use of extra potential SQL attack vector."),
+        "Use of 'extra()' opens a potential SQL attack vector."),
     "S611": QCoreApplication.translate(
         "Security",
-        "Use of RawSQL potential SQL attack vector."),
+        "Use of 'RawSQL()' opens a potential SQL attack vector."),
     
     # Django XSS vulnerability
     "S703": QCoreApplication.translate(
         "Security",
-        "Potential XSS on mark_safe() function."),
+        "Potential XSS on 'mark_safe()' function."),
+    
+    "S999": QCoreApplication.translate(
+        "Security",
+        "{0}: {1}"),
     
 ##    "S": QCoreApplication.translate(
 ##        "Security",
@@ -225,6 +299,11 @@
 }
 
 _securityMessagesSampleArgs = {
+    "S103": ["0o777", "testfile.txt"],
+    "S105": ["password"],
+    "S106": ["password"],
+    "S107": ["password"],
+    
     "S304": ["Crypto.Cipher.DES"],
     "S305": ["cryptography.hazmat.primitives.ciphers.modes.ECB"],
     "S313": ["xml.etree.cElementTree.parse"],
@@ -236,6 +315,8 @@
     "S319": ["xml.dom.pulldom.parse"],
     "S320": ["lxml.etree.parse"],
     
+    "S324": ["MD5"],
+    
     "S403": ["pickle"],
     "S404": ["subprocess"],
     "S405": ["xml.etree.ElementTree"],
@@ -247,4 +328,6 @@
     "S411": ["xmlrpclib"],
     "S412": ["wsgiref.handlers.CGIHandler"],
     "S413": ["Crypto.Cipher"],
+    
+    "S999": ["SyntaxError", "Invalid Syntax"],
 }

eric ide

mercurial