Wed, 10 Jun 2020 17:52:53 +0200
Code Style Checker: continued to implement checker for security related issues.
--- a/eric6.e4p Tue Jun 09 20:10:59 2020 +0200 +++ b/eric6.e4p Wed Jun 10 17:52:53 2020 +0200 @@ -334,7 +334,14 @@ <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/injectionSql.py</Source> + <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/injectionWildcard.py</Source> <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/insecureHashlibNew.py</Source> + <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/insecureSslTls.py</Source> + <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/jinja2Templates.py</Source> + <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/makoTemplates.py</Source> + <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/sshNoHostKeyVerification.py</Source> + <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/tryExcept.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>
--- a/eric6/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleChecker.py Tue Jun 09 20:10:59 2020 +0200 +++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleChecker.py Wed Jun 10 17:52:53 2020 +0200 @@ -111,7 +111,9 @@ flags = [f.strip() for f in comment.split() if (f.startswith("__") and f.endswith("__"))] flags += [f.strip().lower() for f in comment.split() - if f in ("noqa", "NOQA")] + if f in ("noqa", "NOQA", + "nosec", "NOSEC", + "secok", "SECOK")] return flags @@ -130,7 +132,8 @@ if ( "__IGNORE_WARNING__" in lineFlags or - "noqa" in lineFlags + "noqa" in lineFlags or + "nosec" in lineFlags ): # ignore all warning codes return True @@ -145,6 +148,23 @@ return False +def securityOk(code, lineFlags): + """ + Function to check, if the given code is an acknowledged security report. + + @param code error code to be checked + @type str + @param lineFlags list of line flags to check against + @type list of str + @return flag indicating an acknowledged security report + @rtype bool + """ + if lineFlags: + return "secok" in lineFlags + + return False + + def codeStyleCheck(filename, source, args): """ Do the code style check and/or fix found errors. @@ -395,6 +415,15 @@ for lineno, errorsList in errorsDict.items(): errorsList.sort(key=lambda x: x[0], reverse=True) for _, error in errorsList: + error.update({ + "ignored": False, + "fixed": False, + "autofixing": False, + "fixcode": "", + "fixargs": [], + "securityOk": False, + }) + if source: code = error["code"] lineFlags = extractLineFlags(source[lineno - 1].strip()) @@ -403,45 +432,25 @@ flagsLine=True) except IndexError: pass - if not ignoreCode(code, lineFlags): + + if securityOk(code, lineFlags): + error["securityOk"] = True + + if ignoreCode(code, lineFlags): + error["ignored"] = True + else: if fixer: - pass res, fixcode, fixargs, id_ = fixer.fixIssue( lineno, error["offset"], code) if res == -1: deferredFixes[id_] = error else: error.update({ - "ignored": False, "fixed": res == 1, "autofixing": True, "fixcode": fixcode, "fixargs": fixargs, }) - else: - error.update({ - "ignored": False, - "fixed": False, - "autofixing": False, - "fixcode": "", - "fixargs": [], - }) - else: - error.update({ - "ignored": True, - "fixed": False, - "autofixing": False, - "fixcode": "", - "fixargs": [], - }) - else: - error.update({ - "ignored": False, - "fixed": False, - "autofixing": False, - "fixcode": "", - "fixargs": [], - }) results.append(error)
--- a/eric6/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleCheckerDialog.py Tue Jun 09 20:10:59 2020 +0200 +++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleCheckerDialog.py Wed Jun 10 17:52:53 2020 +0200 @@ -246,21 +246,23 @@ self.__lastFileItem.setExpanded(True) self.__lastFileItem.setData(0, self.filenameRole, filename) + msgCode = result["code"].split(".", 1)[0] + fixable = False itm = QTreeWidgetItem( self.__lastFileItem, [ "{0:6}".format(result["line"]), - result["code"], + msgCode, result["display"] ] ) - if result["code"].startswith(("W", "-", "C", "M")): + if msgCode.startswith(("W", "-", "C", "M")): itm.setIcon(1, UI.PixmapCache.getIcon("warning")) - elif result["code"].startswith(("A", "N")): + elif msgCode.startswith(("A", "N")): itm.setIcon(1, UI.PixmapCache.getIcon("namingError")) - elif result["code"].startswith("D"): + elif msgCode.startswith("D"): itm.setIcon(1, UI.PixmapCache.getIcon("docstringError")) - elif result["code"].startswith("S"): + elif msgCode.startswith("S"): if "severity" in result: if result["severity"] == "H": itm.setIcon(1, UI.PixmapCache.getIcon("securityLow")) @@ -277,9 +279,9 @@ if result["fixed"]: itm.setIcon(0, UI.PixmapCache.getIcon("issueFixed")) elif ( - result["code"] in FixableCodeStyleIssues and + msgCode in FixableCodeStyleIssues and not result["autofixing"] and - result["code"] not in self.__noFixCodesList + msgCode not in self.__noFixCodesList ): itm.setIcon(0, UI.PixmapCache.getIcon("issueFixable")) fixable = True @@ -296,7 +298,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"].split(".", 1)[0]) + itm.setData(0, self.codeRole, msgCode) itm.setData(0, self.ignoredRole, result["ignored"]) itm.setData(0, self.argsRole, result["args"]) @@ -327,7 +329,7 @@ itm.setIcon(0, QIcon()) itm.setData(0, self.fixableRole, False) - def __updateStatistics(self, statistics, fixer, ignoredErrors): + def __updateStatistics(self, statistics, fixer, ignoredErrors, securityOk): """ Private method to update the collected statistics. @@ -338,6 +340,8 @@ @type CodeStyleFixer @param ignoredErrors number of ignored errors @type int + @param securityOk number of acknowledged security reports + @type int """ self.__statistics["_FilesCount"] += 1 stats = [k for k in statistics.keys() if k[0].isupper()] @@ -350,6 +354,7 @@ self.__statistics[key] = statistics[key] self.__statistics["_IssuesFixed"] += fixer self.__statistics["_IgnoredErrors"] += ignoredErrors + self.__statistics["_SecurityOK"] += securityOk def __updateFixerStatistics(self, fixer): """ @@ -369,6 +374,7 @@ self.__statistics["_FilesIssues"] = 0 self.__statistics["_IssuesFixed"] = 0 self.__statistics["_IgnoredErrors"] = 0 + self.__statistics["_SecurityOK"] = 0 def prepare(self, fileList, project): """ @@ -812,6 +818,7 @@ fixed = None ignoredErrors = 0 + securityOk = 0 if self.__itms: for itm, result in zip(self.__itms, results): self.__modifyFixedResultItem(itm, result) @@ -828,11 +835,16 @@ ).format(result["display"]) else: continue + + elif result["securityOk"]: + securityOk += 1 + continue + self.results = CodeStyleCheckerDialog.hasResults self.__createResultItem(fn, result) self.__updateStatistics( - codeStyleCheckerStats, fixes, ignoredErrors) + codeStyleCheckerStats, fixes, ignoredErrors, securityOk) if fixed: vm = e5App().getObject("ViewManager")
--- a/eric6/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleCodeSelectionDialog.py Tue Jun 09 20:10:59 2020 +0200 +++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleCodeSelectionDialog.py Wed Jun 10 17:52:53 2020 +0200 @@ -61,7 +61,14 @@ for code in sorted(selectableCodes): message = getTranslatedMessage(code, [], example=True) if message is None: - continue + # try with extension + for ext in ("L", "M", "H", "1"): + message = getTranslatedMessage("{0}.{1}".format(code, ext), + [], example=True) + if message is not None: + break + else: + continue itm = QTreeWidgetItem(self.codeTable, [ code, "\n".join(textWrapper.wrap(message))]) if code.startswith(("W", "C", "M")):
--- a/eric6/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleStatisticsDialog.py Tue Jun 09 20:10:59 2020 +0200 +++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleStatisticsDialog.py Wed Jun 10 17:52:53 2020 +0200 @@ -40,10 +40,12 @@ filesIssues = stats["_FilesIssues"] fixesCount = stats["_IssuesFixed"] ignoresCount = stats["_IgnoredErrors"] + securityOk = stats["_SecurityOK"] del stats["_FilesCount"] del stats["_FilesIssues"] del stats["_IssuesFixed"] del stats["_IgnoredErrors"] + del stats["_SecurityOK"] totalIssues = 0 @@ -68,6 +70,8 @@ self.tr("%n file(s) checked", "", filesCount)) self.filesIssues.setText( self.tr("%n file(s) with issues found", "", filesIssues)) + self.securityOk.setText( + self.tr("%n security issue(s) acknowledged", "", securityOk)) self.statisticsList.resizeColumnToContents(0) self.statisticsList.resizeColumnToContents(1) @@ -93,5 +97,5 @@ elif code.startswith("S"): itm.setIcon(1, UI.PixmapCache.getIcon("securityLow")) - itm.setTextAlignment(0, Qt.AlignRight) - itm.setTextAlignment(1, Qt.AlignHCenter) + itm.setTextAlignment(0, Qt.AlignRight | Qt.AlignVCenter) + itm.setTextAlignment(1, Qt.AlignHCenter | Qt.AlignVCenter)
--- a/eric6/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleStatisticsDialog.ui Tue Jun 09 20:10:59 2020 +0200 +++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleStatisticsDialog.ui Wed Jun 10 17:52:53 2020 +0200 @@ -59,6 +59,9 @@ <item row="2" column="0"> <widget class="QLabel" name="fixedIssues"/> </item> + <item row="2" column="1"> + <widget class="QLabel" name="securityOk"/> + </item> </layout> </item> <item>
--- a/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/generalHardcodedTmp.py Tue Jun 09 20:10:59 2020 +0200 +++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/generalHardcodedTmp.py Wed Jun 10 17:52:53 2020 +0200 @@ -13,7 +13,9 @@ # Original Copyright 2014 Hewlett-Packard Development Company, L.P. # # SPDX-License-Identifier: Apache-2.0 -# +#...r\Security\Checks\generalHardcodedTmp.py + +from Security.SecurityDefaults import SecurityDefaults def getChecks(): @@ -45,7 +47,7 @@ if config and "hardcoded_tmp_directories" in config: tmpDirs = config["hardcoded_tmp_directories"] else: - tmpDirs = ["/tmp", "/var/tmp", "/dev/shm", "~/tmp"] + tmpDirs = SecurityDefaults["hardcoded_tmp_directories"] if any(context.stringVal.startswith(s) for s in tmpDirs): reportError(
--- a/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/injectionShell.py Tue Jun 09 20:10:59 2020 +0200 +++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/injectionShell.py Wed Jun 10 17:52:53 2020 +0200 @@ -19,6 +19,8 @@ import re import sys +from Security.SecurityDefaults import SecurityDefaults + # This regex starts with a windows drive letter (eg C:) # or one of our path delimeter characters (/, \, .) fullPathMatchRe = re.compile(r'^(?:[A-Za-z](?=\:)|[\\\/\.])') @@ -44,62 +46,6 @@ } -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. @@ -168,7 +114,7 @@ if config and "shell_injection_subprocess" in config: functionNames = config["shell_injection_subprocess"] else: - functionNames = _defaultValues("shell_injection_subprocess") + functionNames = SecurityDefaults["shell_injection_subprocess"] if context.callFunctionNameQual in functionNames: shell, shellValue = hasShell(context) @@ -207,7 +153,7 @@ if config and "shell_injection_subprocess" in config: functionNames = config["shell_injection_subprocess"] else: - functionNames = _defaultValues("shell_injection_subprocess") + functionNames = SecurityDefaults["shell_injection_subprocess"] if context.callFunctionNameQual in functionNames: if not hasShell(context)[0]: @@ -234,7 +180,7 @@ if config and "shell_injection_subprocess" in config: functionNames = config["shell_injection_subprocess"] else: - functionNames = _defaultValues("shell_injection_subprocess") + functionNames = SecurityDefaults["shell_injection_subprocess"] if context.callFunctionNameQual not in functionNames: shell, shellValue = hasShell(context) @@ -262,7 +208,7 @@ if config and "shell_injection_shell" in config: functionNames = config["shell_injection_shell"] else: - functionNames = _defaultValues("shell_injection_shell") + functionNames = SecurityDefaults["shell_injection_shell"] if context.callFunctionNameQual in functionNames: if len(context.callArgs) > 0: @@ -299,7 +245,7 @@ if config and "shell_injection_noshell" in config: functionNames = config["shell_injection_noshell"] else: - functionNames = _defaultValues("shell_injection_noshell") + functionNames = SecurityDefaults["shell_injection_noshell"] if context.callFunctionNameQual in functionNames: reportError( @@ -325,17 +271,17 @@ if config and "shell_injection_subprocess" in config: functionNames = config["shell_injection_subprocess"] else: - functionNames = _defaultValues("shell_injection_subprocess") + functionNames = SecurityDefaults["shell_injection_subprocess"] if config and "shell_injection_shell" in config: functionNames += config["shell_injection_shell"] else: - functionNames += _defaultValues("shell_injection_shell") + functionNames += SecurityDefaults["shell_injection_shell"] if config and "shell_injection_noshell" in config: functionNames += config["shell_injection_noshell"] else: - functionNames += _defaultValues("shell_injection_noshell") + functionNames += SecurityDefaults["shell_injection_noshell"] if len(context.callArgs): if context.callFunctionNameQual in functionNames:
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/injectionSql.py Wed Jun 10 17:52:53 2020 +0200 @@ -0,0 +1,118 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a check for SQL 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 + +from Security import SecurityUtils + + +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": [ + (checkHardcodedSqlExpressions, ("S608",)), + ], + } + + +SIMPLE_SQL_RE = re.compile( + r'(select\s.*from\s|' + r'delete\s+from\s|' + r'insert\s+into\s.*values\s|' + r'update\s.*set\s)', + re.IGNORECASE | re.DOTALL, +) + + +def _checkString(data): + """ + Function to check a given string against the list of search patterns. + + @param data string data to be checked + @type str + @return flag indicating a match + @rtype bool + """ + return SIMPLE_SQL_RE.search(data) is not None + + +def _evaluateAst(node): + """ + Function to analyze the given ast node. + + @param node ast node to be analyzed + @type ast.Str + @return tuple containing a flag indicating an execute call and + the resulting statement + @rtype tuple of (bool, str) + """ + wrapper = None + statement = '' + + if isinstance(node._securityParent, ast.BinOp): + out = SecurityUtils.concatString(node, node._securityParent) + wrapper = out[0]._securityParent + statement = out[1] + elif ( + isinstance(node._securityParent, ast.Attribute) and + node._securityParent.attr == 'format' + ): + statement = node.s + # Hierarchy for "".format() is Wrapper -> Call -> Attribute -> Str + wrapper = node._securityParent._securityParent._securityParent + elif ( + hasattr(ast, 'JoinedStr') and + isinstance(node._securityParent, ast.JoinedStr) + ): + statement = node.s + wrapper = node._securityParent._securityParent + + if isinstance(wrapper, ast.Call): # wrapped in "execute" call? + names = ['execute', 'executemany'] + name = SecurityUtils.getCalledName(wrapper) + return (name in names, statement) + else: + return (False, statement) + + +def checkHardcodedSqlExpressions(reportError, context, config): + """ + Function to check for SQL injection. + + @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 + """ + val = _evaluateAst(context.node) + if _checkString(val[1]): + reportError( + context.node.lineno - 1, + context.node.col_offset, + "S608", + "M", + "M" if val[0] else "L", + )
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/injectionWildcard.py Wed Jun 10 17:52:53 2020 +0200 @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a check for use of wildcard 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 +# + +from Security.SecurityDefaults import SecurityDefaults + + +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": [ + (checkLinuxCommandsWildcardInjection, ("S609",)), + ], + } + + +def checkLinuxCommandsWildcardInjection(reportError, context, config): + """ + Function to check for use of wildcard injection. + + @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: + subProcessFunctionNames = config["shell_injection_subprocess"] + else: + subProcessFunctionNames = SecurityDefaults[ + "shell_injection_subprocess"] + + if config and "shell_injection_shell" in config: + shellFunctionNames = config["shell_injection_shell"] + else: + shellFunctionNames = SecurityDefaults["shell_injection_shell"] + + vulnerableFunctions = ['chown', 'chmod', 'tar', 'rsync'] + if ( + context.callFunctionNameQual in shellFunctionNames or + (context.callFunctionNameQual in subProcessFunctionNames and + context.checkCallArgValue('shell', 'True')) + ): + if context.callArgsCount >= 1: + callArgument = context.getCallArgAtPosition(0) + argumentString = '' + if isinstance(callArgument, list): + for li in callArgument: + argumentString = argumentString + ' {0}'.format(li) + elif isinstance(callArgument, str): + argumentString = callArgument + + if argumentString != '': + for vulnerableFunction in vulnerableFunctions: + if ( + vulnerableFunction in argumentString and + '*' in argumentString + ): + lineNo = context.getLinenoForCallArg('shell') + if lineNo < 1: + lineNo = context.node.lineno + offset = context.getOffsetForCallArg('shell') + if offset < 0: + offset = context.node.col_offset + reportError( + lineNo - 1, + offset, + "S609", + "H", + "M", + context.callFunctionNameQual + )
--- a/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/insecureHashlibNew.py Tue Jun 09 20:10:59 2020 +0200 +++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/insecureHashlibNew.py Wed Jun 10 17:52:53 2020 +0200 @@ -16,6 +16,8 @@ # SPDX-License-Identifier: Apache-2.0 # +from Security.SecurityDefaults import SecurityDefaults + def getChecks(): """ @@ -47,7 +49,7 @@ if config and "insecure_hashes" in config: insecureHashes = [h.lower() for h in config["insecure_hashes"]] else: - insecureHashes = ['md4', 'md5', 'sha', 'sha1'] + insecureHashes = SecurityDefaults["insecure_hashes"] if isinstance(context.callFunctionNameQual, str): qualnameList = context.callFunctionNameQual.split('.')
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/insecureSslTls.py Wed Jun 10 17:52:53 2020 +0200 @@ -0,0 +1,153 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a check for use of SSL/TLS with insecure protocols. +""" + +# +# 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 +# + +from Security.SecurityDefaults import SecurityDefaults + + +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": [ + (checkInsecureSslProtocolVersion, ("S502",)), + (checkSslWithoutVersion, ("S504",)), + ], + "FunctionDef": [ + (checkInsecureSslDefaults, ("S503",)), + ], + } + + +def checkInsecureSslProtocolVersion(reportError, context, config): + """ + Function to check for use of insecure SSL protocol version. + + @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_ssl_protocol_versions" in config: + insecureProtocolVersions = config["insecure_ssl_protocol_versions"] + else: + insecureProtocolVersions = SecurityDefaults[ + "insecure_ssl_protocol_versions"] + + if context.callFunctionNameQual == 'ssl.wrap_socket': + if context.checkCallArgValue('ssl_version', insecureProtocolVersions): + reportError( + context.getLinenoForCallArg('ssl_version') - 1, + context.getOffsetForCallArg('ssl_version'), + "S502.1", + "H", + "H", + ) + + elif context.callFunctionNameQual == 'pyOpenSSL.SSL.Context': + if context.checkCallArgValue('method', insecureProtocolVersions): + reportError( + context.getLinenoForCallArg('method') - 1, + context.getOffsetForCallArg('method'), + "S502.2", + "H", + "H", + ) + + elif ( + context.callFunctionNameQual != 'ssl.wrap_socket' and + context.callFunctionNameQual != 'pyOpenSSL.SSL.Context' + ): + if context.checkCallArgValue('method', insecureProtocolVersions): + reportError( + context.getLinenoForCallArg('method') - 1, + context.getOffsetForCallArg('method'), + "S502.3", + "H", + "H", + ) + + elif context.checkCallArgValue('ssl_version', + insecureProtocolVersions): + reportError( + context.getLinenoForCallArg('ssl_version') - 1, + context.getOffsetForCallArg('ssl_version'), + "S502.3", + "H", + "H", + ) + + +def checkInsecureSslDefaults(reportError, context, config): + """ + Function to check for SSL use with insecure defaults specified. + + @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_ssl_protocol_versions" in config: + insecureProtocolVersions = config["insecure_ssl_protocol_versions"] + else: + insecureProtocolVersions = SecurityDefaults[ + "insecure_ssl_protocol_versions"] + + for default in context.functionDefDefaultsQual: + val = default.split(".")[-1] + if val in insecureProtocolVersions: + reportError( + context.node.lineno - 1, + context.node.col_offset, + "S503", + "M", + "M", + ) + + +def checkSslWithoutVersion(reportError, context, config): + """ + Function to check for SSL use with no version specified. + + @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.callFunctionNameQual == 'ssl.wrap_socket': + if context.checkCallArgValue('ssl_version') is None: + # checkCallArgValue() returns False if the argument is found + # but does not match the supplied value (or the default None). + # It returns None if the argument passed doesn't exist. This + # tests for that (ssl_version is not specified). + reportError( + context.node.lineno - 1, + context.node.col_offset, + "S504", + "L", + "M", + )
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/jinja2Templates.py Wed Jun 10 17:52:53 2020 +0200 @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a check for not auto escaping in jinja2. +""" + +# +# 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 + + +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": [ + (checkJinja2Autoescape, ("S701",)), + ], + } + + +def checkJinja2Autoescape(reportError, context, config): + """ + Function to check for not auto escaping in jinja2. + + @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 isinstance(context.callFunctionNameQual, str): + qualnameList = context.callFunctionNameQual.split('.') + func = qualnameList[-1] + if 'jinja2' in qualnameList and func == 'Environment': + for node in ast.walk(context.node): + if isinstance(node, ast.keyword): + # definite autoescape = False + if ( + getattr(node, 'arg', None) == 'autoescape' and + ( + getattr(node.value, 'id', None) == 'False' or + getattr(node.value, 'value', None) is False + ) + ): + reportError( + context.node.lineno - 1, + context.node.col_offset, + "S701.1", + "H", + "H", + ) + return + + # found autoescape + if getattr(node, 'arg', None) == 'autoescape': + value = getattr(node, 'value', None) + if ( + getattr(value, 'id', None) == 'True' or + getattr(value, 'value', None) is True + ): + return + + # Check if select_autoescape function is used. + elif ( + isinstance(value, ast.Call) and + (getattr(value.func, 'id', None) == + 'select_autoescape') + ): + return + + else: + reportError( + context.node.lineno - 1, + context.node.col_offset, + "S701.1", + "H", + "M", + ) + return + + # We haven't found a keyword named autoescape, indicating default + # behavior + reportError( + context.node.lineno - 1, + context.node.col_offset, + "S701.2", + "H", + "H", + )
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/makoTemplates.py Wed Jun 10 17:52:53 2020 +0200 @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a check for use of mako templates. +""" + +# +# 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": [ + (checkMakoTemplateUsage, ("S702",)), + ], + } + + +def checkMakoTemplateUsage(reportError, context, config): + """ + Function to check for use of mako templates. + + @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 isinstance(context.callFunctionNameQual, str): + qualnameList = context.callFunctionNameQual.split('.') + func = qualnameList[-1] + if 'mako' in qualnameList and func == 'Template': + # unlike Jinja2, mako does not have a template wide autoescape + # feature and thus each variable must be carefully sanitized. + reportError( + context.node.lineno - 1, + context.node.col_offset, + "S702", + "M", + "H", + )
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/sshNoHostKeyVerification.py Wed Jun 10 17:52:53 2020 +0200 @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a check for use of mako templates. +""" + +# +# 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": [ + (checkSshNoHostKeyVerification, ("S507",)), + ], + } + + +def checkSshNoHostKeyVerification(reportError, context, config): + """ + Function to check for use of mako templates. + + @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.isModuleImportedLike('paramiko') and + context.callFunctionName == 'set_missing_host_key_policy' + ): + if ( + context.callArgs and + context.callArgs[0] in ['AutoAddPolicy', 'WarningPolicy'] + ): + reportError( + context.node.lineno - 1, + context.node.col_offset, + "S507", + "H", + "M", + )
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/tryExcept.py Wed Jun 10 17:52:53 2020 +0200 @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing checks for insecure except blocks. +""" + +# +# 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 + +from Security.SecurityDefaults import SecurityDefaults + + +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 { + "ExceptHandler": [ + (checkTryExceptPass, ("S110",)), + (checkTryExceptContinue, ("S112",)), + ], + } + + +def checkTryExceptPass(reportError, context, config): + """ + Function to check for a pass in the except block. + + @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 "check_typed_exception" in config: + checkTypedException = config["check_typed_exception"] + else: + checkTypedException = SecurityDefaults["check_typed_exception"] + + node = context.node + if len(node.body) == 1: + if ( + not checkTypedException and + node.type is not None and + getattr(node.type, 'id', None) != 'Exception' + ): + return + + if isinstance(node.body[0], ast.Pass): + reportError( + context.node.lineno - 1, + context.node.col_offset, + "S110", + "L", + "H", + ) + + +def checkTryExceptContinue(reportError, context, config): + """ + Function to check for a continue in the except block. + + @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 "check_typed_exception" in config: + checkTypedException = config["check_typed_exception"] + else: + checkTypedException = SecurityDefaults["check_typed_exception"] + + node = context.node + if len(node.body) == 1: + if ( + not checkTypedException and + node.type is not None and + getattr(node.type, 'id', None) != 'Exception' + ): + return + + if isinstance(node.body[0], ast.Continue): + reportError( + context.node.lineno - 1, + context.node.col_offset, + "S112", + "L", + "H", + )
--- a/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/SecurityChecker.py Tue Jun 09 20:10:59 2020 +0200 +++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/SecurityChecker.py Wed Jun 10 17:52:53 2020 +0200 @@ -38,6 +38,9 @@ # hardcoded tmp directory "S108", + # try-except + "S110", "S112", + # flask app "S201", @@ -55,17 +58,38 @@ # insecure certificate usage "S501", + # insecure SSL/TLS protocol version + "S502", "S503", "S504", + # YAML load "S506", + # SSH host key verification + "S507", + # Shell injection "S601", "S602", "S603", "S604", "S605", "S606", "S607", + # SQL injection + "S608", + + # Wildcard injection + "S609", + # Django SQL injection "S610", "S611", + # Jinja2 templates + "S701", + + # Mako templates + "S702", + # Django XSS vulnerability "S703", + + # Syntax error + "S999", ] def __init__(self, source, filename, select, ignore, expected, repeat,
--- a/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/SecurityDefaults.py Tue Jun 09 20:10:59 2020 +0200 +++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/SecurityDefaults.py Wed Jun 10 17:52:53 2020 +0200 @@ -8,14 +8,23 @@ """ SecurityDefaults = { + # generalHardcodedTmp.py "hardcoded_tmp_directories": ["/tmp", "/var/tmp", "/dev/shm", "~/tmp"], + + # insecureHashlibNew.py "insecure_hashes": ['md4', 'md5', 'sha', 'sha1'], + + # injectionShell.py + # injectionWildcard.py "shell_injection_subprocess": [ 'subprocess.Popen', 'subprocess.call', 'subprocess.check_call', 'subprocess.check_output', 'subprocess.run'], + + # injectionShell.py + # injectionWildcard.py "shell_injection_shell": [ 'os.system', 'os.popen', @@ -29,6 +38,8 @@ 'popen2.Popen4', 'commands.getoutput', 'commands.getstatusoutput'], + + # injectionShell.py "shell_injection_noshell": [ 'os.execl', 'os.execle', @@ -47,4 +58,17 @@ 'os.spawnvp', 'os.spawnvpe', 'os.startfile'], + + # insecureSslTls.py + "insecure_ssl_protocol_versions": [ + 'PROTOCOL_SSLv2', + 'SSLv2_METHOD', + 'SSLv23_METHOD', + 'PROTOCOL_SSLv3', + 'PROTOCOL_TLSv1', + 'SSLv3_METHOD', + 'TLSv1_METHOD'], + + # tryExcept.py + "check_typed_exception": False, }
--- a/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/SecurityUtils.py Tue Jun 09 20:10:59 2020 +0200 +++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/SecurityUtils.py Wed Jun 10 17:52:53 2020 +0200 @@ -258,3 +258,67 @@ @rtype bytes """ return b.decode('unicode_escape').encode('unicode_escape') + + +def concatString(node, stop=None): + """ + Function to build a string from an ast.BinOp chain. + + This will build a string from a series of ast.Str nodes wrapped in + ast.BinOp nodes. Something like "a" + "b" + "c" or "a %s" % val etc. + The provided node can be any participant in the BinOp chain. + + @param node node to be processed + @type ast.BinOp or ast.Str + @param stop base node to stop at + @type ast.BinOp or ast.Str + @return tuple containing the root node of the expression and the string + value + @rtype tuple of (ast.AST, str) + """ + def _get(node, bits, stop=None): + if node != stop: + bits.append( + _get(node.left, bits, stop) + if isinstance(node.left, ast.BinOp) + else node.left + ) + bits.append( + _get(node.right, bits, stop) + if isinstance(node.right, ast.BinOp) + else node.right + ) + + bits = [node] + while isinstance(node._securityParent, ast.BinOp): + node = node._securityParent + if isinstance(node, ast.BinOp): + _get(node, bits, stop) + + return ( + node, + " ".join([x.s for x in bits if isinstance(x, ast.Str)]) + ) + + +def getCalledName(node): + """ + Function to get the function name from an ast.Call node. + + An ast.Call node representing a method call will present differently to one + wrapping a function call: thing.call() vs call(). This helper will grab the + unqualified call name correctly in either case. + + @param node reference to the call node + @type ast.Call + @return function name of the node + @rtype str + """ + func = node.func + try: + return func.attr if isinstance(func, ast.Attribute) else func.id + except AttributeError: + return "" + +# +# eflag: noqa = M601
--- a/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/translations.py Tue Jun 09 20:10:59 2020 +0200 +++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/translations.py Wed Jun 10 17:52:53 2020 +0200 @@ -49,6 +49,14 @@ "Security", "Probable insecure usage of temp file/directory."), + # try-except + "S110": QCoreApplication.translate( + "Security", + "Try, Except, Pass detected."), + "S112": QCoreApplication.translate( + "Security", + "Try, Except, Continue detected."), + # flask app "S201": QCoreApplication.translate( "Security", @@ -236,12 +244,40 @@ "'requests' call with verify=False disabling SSL certificate checks," " security issue."), + # insecure SSL/TLS protocol version + "S502.1": QCoreApplication.translate( + "Security", + "'ssl.wrap_socket' call with insecure SSL/TLS protocol version" + " identified, security issue."), + "S502.2": QCoreApplication.translate( + "Security", + "'SSL.Context' call with insecure SSL/TLS protocol version identified," + " security issue."), + "S502.3": QCoreApplication.translate( + "Security", + "Function call with insecure SSL/TLS protocol version identified," + " security issue."), + "S503": QCoreApplication.translate( + "Security", + "Function definition identified with insecure SSL/TLS protocol" + " version by default, possible security issue."), + "S504": QCoreApplication.translate( + "Security", + "'ssl.wrap_socket' call with no SSL/TLS protocol version specified," + " the default 'SSLv23' could be insecure, possible security issue."), + # YAML load "S506": QCoreApplication.translate( "Security", "Use of unsafe 'yaml.load()'. Allows instantiation of arbitrary" " objects. Consider 'yaml.safe_load()'."), + # SSH host key verification + "S507": QCoreApplication.translate( + "Security", + "Paramiko call with policy set to automatically trust the unknown" + " host key."), + # Shell injection "S601": QCoreApplication.translate( "Security", @@ -276,6 +312,17 @@ "Security", "Starting a process with a partial executable path."), + # SQL injection + "S608": QCoreApplication.translate( + "Security", + "Possible SQL injection vector through string-based query" + " construction."), + + # Wildcard injection + "S609": QCoreApplication.translate( + "Security", + "Possible wildcard injection in call: {0}"), + # Django SQL injection "S610": QCoreApplication.translate( "Security", @@ -284,11 +331,32 @@ "Security", "Use of 'RawSQL()' opens a potential SQL attack vector."), + # Jinja2 templates + "S701.1": QCoreApplication.translate( + "Security", + "Using jinja2 templates with 'autoescape=False' is dangerous and can" + " lead to XSS. Use 'autoescape=True' or use the 'select_autoescape'" + " function to mitigate XSS vulnerabilities."), + "S701.2": QCoreApplication.translate( + "Security", + "By default, jinja2 sets 'autoescape' to False. Consider using" + " 'autoescape=True' or use the 'select_autoescape' function to" + " mitigate XSS vulnerabilities."), + + # Mako templates + "S702": QCoreApplication.translate( + "Security", + "Mako templates allow HTML/JS rendering by default and are inherently" + " open to XSS attacks. Ensure variables in all templates are properly" + " sanitized via the 'n', 'h' or 'x' flags (depending on context). For" + " example, to HTML escape the variable 'data' do ${{ data |h }}."), + # Django XSS vulnerability "S703": QCoreApplication.translate( "Security", "Potential XSS on 'mark_safe()' function."), + # Syntax error "S999": QCoreApplication.translate( "Security", "{0}: {1}"), @@ -329,5 +397,7 @@ "S412": ["wsgiref.handlers.CGIHandler"], "S413": ["Crypto.Cipher"], + "S609": ["os.system"], + "S999": ["SyntaxError", "Invalid Syntax"], }