diff --git a/rules/python/security/python-mysql-empty-password-python.yml b/rules/python/security/python-mysql-empty-password-python.yml new file mode 100644 index 00000000..495f4c48 --- /dev/null +++ b/rules/python/security/python-mysql-empty-password-python.yml @@ -0,0 +1,88 @@ +id: python-mysql-empty-password-python +language: python +severity: warning +message: >- + The application creates a database connection with an empty password. This can lead to unauthorized access by either an internal or external malicious actor. To prevent this vulnerability, enforce authentication when connecting to a database by using environment variables to securely provide credentials or retrieving them from a secure vault or HSM (Hardware Security Module). +note: >- + [CWE-287]: Improper Authentication + [A07:2021]: Identification and Authentication Failures + [REFERENCES] + - https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html + +rule: + any: + - kind: call + has: + kind: attribute + field: function + regex: ^mysql.connector.connect$ + precedes: + kind: argument_list + has: + kind: keyword_argument + all: + - has: + kind: identifier + nthChild: 1 + regex: ^(password|passwd)$ + - has: + kind: string + nthChild: 2 + all: + - has: + kind: string_start + nthChild: 1 + - has: + kind: string_end + nthChild: 2 + inside: + stopBy: end + follows: + stopBy: end + kind: import_statement + has: + kind: dotted_name + nthChild: 1 + regex: ^mysql.connector$ + - kind: call + has: + kind: attribute + field: function + pattern: $CONNECTOR_ALIAS.connect + precedes: + kind: argument_list + has: + kind: keyword_argument + all: + - has: + kind: identifier + nthChild: 1 + regex: ^(password|passwd)$ + - has: + kind: string + nthChild: 2 + all: + - has: + kind: string_start + nthChild: 1 + - has: + kind: string_end + nthChild: 2 + inside: + stopBy: end + follows: + stopBy: end + kind: import_statement + has: + kind: aliased_import + nthChild: 1 + all: + - has: + kind: dotted_name + nthChild: 1 + regex: ^mysql.connector$ + - has: + kind: identifier + field: alias + nthChild: 2 + pattern: $CONNECTOR_ALIAS diff --git a/rules/python/security/python-mysql-hardcoded-secret-python.yml b/rules/python/security/python-mysql-hardcoded-secret-python.yml new file mode 100644 index 00000000..b21b3121 --- /dev/null +++ b/rules/python/security/python-mysql-hardcoded-secret-python.yml @@ -0,0 +1,94 @@ +id: python-mysql-hardcoded-secret-python +language: python +severity: warning +message: >- + A secret is hard-coded in the application. Secrets stored in source code, such as credentials, identifiers, and other types of sensitive data, can be leaked and used by internal or external malicious actors. Use environment variables to securely provide credentials and other secrets or retrieve them from a secure vault or Hardware Security Module (HSM). +note: >- + [CWE-798]: Use of Hard-coded Credentials + [A07:2021]: Identification and Authentication Failures + [REFERENCES] + - https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html + +rule: + any: + - kind: call + has: + kind: attribute + field: function + regex: ^mysql.connector.connect$ + precedes: + kind: argument_list + has: + kind: keyword_argument + all: + - has: + kind: identifier + nthChild: 1 + regex: ^(password|passwd)$ + - has: + kind: string + nthChild: 2 + all: + - has: + kind: string_start + nthChild: 1 + - has: + kind: string_content + nthChild: 2 + - has: + kind: string_end + nthChild: 3 + inside: + stopBy: end + follows: + stopBy: end + kind: import_statement + has: + kind: dotted_name + nthChild: 1 + regex: ^mysql.connector$ + - kind: call + has: + kind: attribute + field: function + pattern: $CONNECTOR_ALIAS.connect + precedes: + kind: argument_list + has: + kind: keyword_argument + all: + - has: + kind: identifier + nthChild: 1 + regex: ^(password|passwd)$ + - has: + kind: string + nthChild: 2 + all: + - has: + kind: string_start + nthChild: 1 + - has: + kind: string_content + nthChild: 2 + - has: + kind: string_end + nthChild: 3 + inside: + stopBy: end + follows: + stopBy: end + kind: import_statement + has: + kind: aliased_import + nthChild: 1 + all: + - has: + kind: dotted_name + nthChild: 1 + regex: ^mysql.connector$ + - has: + kind: identifier + field: alias + nthChild: 2 + pattern: $CONNECTOR_ALIAS diff --git a/rules/python/security/python-mysqlclient-empty-password-python.yml b/rules/python/security/python-mysqlclient-empty-password-python.yml new file mode 100644 index 00000000..76a80a48 --- /dev/null +++ b/rules/python/security/python-mysqlclient-empty-password-python.yml @@ -0,0 +1,214 @@ +id: python-mysqlclient-empty-password-python +language: python +severity: warning +message: >- + The application creates a database connection with an empty password. This can lead to unauthorized access by either an internal or external malicious actor. To prevent this vulnerability, enforce authentication when connecting to a database by using environment variables to securely provide credentials or retrieving them from a secure vault or HSM (Hardware Security Module). +note: >- + [CWE-287]: Improper Authentication + [A07:2021]: Identification and Authentication Failures + [REFERENCES] + - https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html + +rule: + kind: call + any: + - all: + - has: + nthChild: 1 + kind: attribute + pattern: MySQLdb.$CONNECT + - has: + kind: argument_list + nthChild: 2 + has: + any: + - kind: keyword_argument + all: + - has: + kind: identifier + field: name + nthChild: 1 + regex: ^passwd$ + - has: + kind: string + field: value + nthChild: 2 + all: + - has: + kind: string_start + nthChild: 1 + - has: + kind: string_end + nthChild: 2 + - kind: string + nthChild: 3 + all: + - has: + kind: string_start + nthChild: 1 + - has: + kind: string_end + nthChild: 2 + - all: + - has: + nthChild: 1 + kind: attribute + pattern: $MYSQL_ALIAS.$CONNECT + inside: + stopBy: end + follows: + stopBy: end + kind: import_statement + has: + kind: aliased_import + all: + - has: + kind: dotted_name + regex: MySQLdb + - has: + kind: identifier + pattern: $MYSQL_ALIAS + - has: + kind: argument_list + nthChild: 2 + has: + any: + - kind: keyword_argument + all: + - has: + kind: identifier + field: name + nthChild: 1 + regex: ^passwd$ + - has: + kind: string + field: value + nthChild: 2 + all: + - has: + kind: string_start + nthChild: 1 + - has: + kind: string_end + nthChild: 2 + - kind: string + nthChild: 3 + all: + - has: + kind: string_start + nthChild: 1 + - has: + kind: string_end + nthChild: 2 + - all: + - has: + nthChild: 1 + kind: attribute + any: + - pattern: MySQLdb._mysql.$CONNECT + - pattern: _mysql.$CONNECT + inside: + stopBy: end + follows: + stopBy: end + kind: import_from_statement + all: + - has: + kind: dotted_name + field: module_name + regex: ^MySQLdb$ + - has: + kind: dotted_name + field: name + regex: ^_mysql$ + - has: + kind: argument_list + nthChild: 2 + has: + any: + - kind: keyword_argument + all: + - has: + kind: identifier + field: name + nthChild: 1 + regex: ^passwd$ + - has: + kind: string + field: value + nthChild: 2 + all: + - has: + kind: string_start + nthChild: 1 + - has: + kind: string_end + nthChild: 2 + - kind: string + nthChild: 3 + all: + - has: + kind: string_start + nthChild: 1 + - has: + kind: string_end + nthChild: 2 + - all: + - has: + nthChild: 1 + kind: attribute + pattern: $MYSQL_FROM_ALIAS.$CONNECT + inside: + stopBy: end + follows: + stopBy: end + kind: import_from_statement + all: + - has: + kind: dotted_name + field: module_name + regex: ^MySQLdb$ + nthChild: 1 + - has: + kind: aliased_import + all: + - has: + kind: dotted_name + field: name + regex: ^_mysql$ + - has: + kind: identifier + field: alias + pattern: $MYSQL_FROM_ALIAS + - has: + kind: argument_list + nthChild: 2 + has: + any: + - kind: keyword_argument + all: + - has: + kind: identifier + field: name + nthChild: 1 + regex: ^passwd$ + - has: + kind: string + field: value + nthChild: 2 + all: + - has: + kind: string_start + nthChild: 1 + - has: + kind: string_end + nthChild: 2 + - kind: string + nthChild: 3 + all: + - has: + kind: string_start + nthChild: 1 + - has: + kind: string_end + nthChild: 2 diff --git a/tests/__snapshots__/python-mysql-empty-password-python-snapshot.yml b/tests/__snapshots__/python-mysql-empty-password-python-snapshot.yml new file mode 100644 index 00000000..5a113ed5 --- /dev/null +++ b/tests/__snapshots__/python-mysql-empty-password-python-snapshot.yml @@ -0,0 +1,107 @@ +id: python-mysql-empty-password-python +snapshots: + ? | + import mysql.connector + conn = mysql.connector.connect(username="abcz", passwd="") + : labels: + - source: mysql.connector.connect(username="abcz", passwd="") + style: primary + start: 30 + end: 81 + - source: mysql.connector + style: secondary + start: 7 + end: 22 + - source: import mysql.connector + style: secondary + start: 0 + end: 22 + - source: import mysql.connector + style: secondary + start: 0 + end: 22 + - source: passwd + style: secondary + start: 71 + end: 77 + - source: '"' + style: secondary + start: 78 + end: 79 + - source: '"' + style: secondary + start: 79 + end: 80 + - source: '""' + style: secondary + start: 78 + end: 80 + - source: passwd="" + style: secondary + start: 71 + end: 80 + - source: (username="abcz", passwd="") + style: secondary + start: 53 + end: 81 + - source: mysql.connector.connect + style: secondary + start: 30 + end: 53 + ? | + import mysql.connector as mysql123 + def my_function(): + mysql123.connect(host="localhost",user="root",passwd="",database="aaa") + : labels: + - source: mysql123.connect(host="localhost",user="root",passwd="",database="aaa") + style: primary + start: 56 + end: 127 + - source: mysql.connector + style: secondary + start: 7 + end: 22 + - source: mysql123 + style: secondary + start: 26 + end: 34 + - source: mysql.connector as mysql123 + style: secondary + start: 7 + end: 34 + - source: import mysql.connector as mysql123 + style: secondary + start: 0 + end: 34 + - source: import mysql.connector as mysql123 + style: secondary + start: 0 + end: 34 + - source: passwd + style: secondary + start: 102 + end: 108 + - source: '"' + style: secondary + start: 109 + end: 110 + - source: '"' + style: secondary + start: 110 + end: 111 + - source: '""' + style: secondary + start: 109 + end: 111 + - source: passwd="" + style: secondary + start: 102 + end: 111 + - source: (host="localhost",user="root",passwd="",database="aaa") + style: secondary + start: 72 + end: 127 + - source: mysql123.connect + style: secondary + start: 56 + end: 72 diff --git a/tests/__snapshots__/python-mysql-hardcoded-secret-python-snapshot.yml b/tests/__snapshots__/python-mysql-hardcoded-secret-python-snapshot.yml new file mode 100644 index 00000000..8f078f74 --- /dev/null +++ b/tests/__snapshots__/python-mysql-hardcoded-secret-python-snapshot.yml @@ -0,0 +1,115 @@ +id: python-mysql-hardcoded-secret-python +snapshots: + ? | + import mysql.connector + conn = mysql.connector.connect(username="abcz", passwd="abc") + : labels: + - source: mysql.connector.connect(username="abcz", passwd="abc") + style: primary + start: 30 + end: 84 + - source: mysql.connector + style: secondary + start: 7 + end: 22 + - source: import mysql.connector + style: secondary + start: 0 + end: 22 + - source: import mysql.connector + style: secondary + start: 0 + end: 22 + - source: passwd + style: secondary + start: 71 + end: 77 + - source: '"' + style: secondary + start: 78 + end: 79 + - source: abc + style: secondary + start: 79 + end: 82 + - source: '"' + style: secondary + start: 82 + end: 83 + - source: '"abc"' + style: secondary + start: 78 + end: 83 + - source: passwd="abc" + style: secondary + start: 71 + end: 83 + - source: (username="abcz", passwd="abc") + style: secondary + start: 53 + end: 84 + - source: mysql.connector.connect + style: secondary + start: 30 + end: 53 + ? | + import mysql.connector as mysql123 + def my_function(): + mysql123.connect(host="localhost",user="root",passwd="abc",database="aaa") + : labels: + - source: mysql123.connect(host="localhost",user="root",passwd="abc",database="aaa") + style: primary + start: 56 + end: 130 + - source: mysql.connector + style: secondary + start: 7 + end: 22 + - source: mysql123 + style: secondary + start: 26 + end: 34 + - source: mysql.connector as mysql123 + style: secondary + start: 7 + end: 34 + - source: import mysql.connector as mysql123 + style: secondary + start: 0 + end: 34 + - source: import mysql.connector as mysql123 + style: secondary + start: 0 + end: 34 + - source: passwd + style: secondary + start: 102 + end: 108 + - source: '"' + style: secondary + start: 109 + end: 110 + - source: abc + style: secondary + start: 110 + end: 113 + - source: '"' + style: secondary + start: 113 + end: 114 + - source: '"abc"' + style: secondary + start: 109 + end: 114 + - source: passwd="abc" + style: secondary + start: 102 + end: 114 + - source: (host="localhost",user="root",passwd="abc",database="aaa") + style: secondary + start: 72 + end: 130 + - source: mysql123.connect + style: secondary + start: 56 + end: 72 diff --git a/tests/__snapshots__/python-mysqlclient-empty-password-python-snapshot.yml b/tests/__snapshots__/python-mysqlclient-empty-password-python-snapshot.yml new file mode 100644 index 00000000..3362ee55 --- /dev/null +++ b/tests/__snapshots__/python-mysqlclient-empty-password-python-snapshot.yml @@ -0,0 +1,350 @@ +id: python-mysqlclient-empty-password-python +snapshots: + ? | + from MySQLdb import _mysql + db = MySQLdb._mysql.connect('', '', "", '') + : labels: + - source: MySQLdb._mysql.connect('', '', "", '') + style: primary + start: 32 + end: 70 + - source: MySQLdb._mysql.connect + style: secondary + start: 32 + end: 54 + - source: '"' + style: secondary + start: 63 + end: 64 + - source: '"' + style: secondary + start: 64 + end: 65 + - source: '""' + style: secondary + start: 63 + end: 65 + - source: ('', '', "", '') + style: secondary + start: 54 + end: 70 + ? | + from MySQLdb import _mysql + db = _mysql.connect( + host=FLAGS.host, user=FLAGS.user, passwd="", db=FLAGS.db + ) + : labels: + - source: |- + _mysql.connect( + host=FLAGS.host, user=FLAGS.user, passwd="", db=FLAGS.db + ) + style: primary + start: 32 + end: 106 + - source: MySQLdb + style: secondary + start: 5 + end: 12 + - source: _mysql + style: secondary + start: 20 + end: 26 + - source: from MySQLdb import _mysql + style: secondary + start: 0 + end: 26 + - source: from MySQLdb import _mysql + style: secondary + start: 0 + end: 26 + - source: _mysql.connect + style: secondary + start: 32 + end: 46 + - source: passwd + style: secondary + start: 82 + end: 88 + - source: '"' + style: secondary + start: 89 + end: 90 + - source: '"' + style: secondary + start: 90 + end: 91 + - source: '""' + style: secondary + start: 89 + end: 91 + - source: passwd="" + style: secondary + start: 82 + end: 91 + - source: |- + ( + host=FLAGS.host, user=FLAGS.user, passwd="", db=FLAGS.db + ) + style: secondary + start: 46 + end: 106 + ? | + from MySQLdb import _mysql as mysql + db = mysql.connect( + host=FLAGS.host, user=FLAGS.user, passwd="", db=FLAGS.db + ) + : labels: + - source: |- + mysql.connect( + host=FLAGS.host, user=FLAGS.user, passwd="", db=FLAGS.db + ) + style: primary + start: 41 + end: 116 + - source: MySQLdb + style: secondary + start: 5 + end: 12 + - source: _mysql + style: secondary + start: 20 + end: 26 + - source: mysql + style: secondary + start: 30 + end: 35 + - source: _mysql as mysql + style: secondary + start: 20 + end: 35 + - source: from MySQLdb import _mysql as mysql + style: secondary + start: 0 + end: 35 + - source: from MySQLdb import _mysql as mysql + style: secondary + start: 0 + end: 35 + - source: mysql.connect + style: secondary + start: 41 + end: 54 + - source: passwd + style: secondary + start: 92 + end: 98 + - source: '"' + style: secondary + start: 99 + end: 100 + - source: '"' + style: secondary + start: 100 + end: 101 + - source: '""' + style: secondary + start: 99 + end: 101 + - source: passwd="" + style: secondary + start: 92 + end: 101 + - source: |- + ( + host=FLAGS.host, user=FLAGS.user, passwd="", db=FLAGS.db + ) + style: secondary + start: 54 + end: 116 + ? | + from MySQLdb import _mysql as mysql + db = mysql.connect("MYSQL_HOST", "MYSQL_USER", "", "MYSQL_DATABASE") + : labels: + - source: mysql.connect("MYSQL_HOST", "MYSQL_USER", "", "MYSQL_DATABASE") + style: primary + start: 41 + end: 104 + - source: MySQLdb + style: secondary + start: 5 + end: 12 + - source: _mysql + style: secondary + start: 20 + end: 26 + - source: mysql + style: secondary + start: 30 + end: 35 + - source: _mysql as mysql + style: secondary + start: 20 + end: 35 + - source: from MySQLdb import _mysql as mysql + style: secondary + start: 0 + end: 35 + - source: from MySQLdb import _mysql as mysql + style: secondary + start: 0 + end: 35 + - source: mysql.connect + style: secondary + start: 41 + end: 54 + - source: '"' + style: secondary + start: 83 + end: 84 + - source: '"' + style: secondary + start: 84 + end: 85 + - source: '""' + style: secondary + start: 83 + end: 85 + - source: ("MYSQL_HOST", "MYSQL_USER", "", "MYSQL_DATABASE") + style: secondary + start: 54 + end: 104 + ? | + import MySQLdb + db = MySQLdb.Connection(host="127.0.0.1", user="root", passwd="", db="business") + : labels: + - source: MySQLdb.Connection(host="127.0.0.1", user="root", passwd="", db="business") + style: primary + start: 20 + end: 95 + - source: MySQLdb.Connection + style: secondary + start: 20 + end: 38 + - source: passwd + style: secondary + start: 70 + end: 76 + - source: '"' + style: secondary + start: 77 + end: 78 + - source: '"' + style: secondary + start: 78 + end: 79 + - source: '""' + style: secondary + start: 77 + end: 79 + - source: passwd="" + style: secondary + start: 70 + end: 79 + - source: (host="127.0.0.1", user="root", passwd="", db="business") + style: secondary + start: 38 + end: 95 + ? | + import MySQLdb as myalias + conn = myalias.Connection('127.0.0.1', 'root', '', 'mail') + : labels: + - source: myalias.Connection('127.0.0.1', 'root', '', 'mail') + style: primary + start: 33 + end: 84 + - source: MySQLdb + style: secondary + start: 7 + end: 14 + - source: myalias + style: secondary + start: 18 + end: 25 + - source: MySQLdb as myalias + style: secondary + start: 7 + end: 25 + - source: import MySQLdb as myalias + style: secondary + start: 0 + end: 25 + - source: import MySQLdb as myalias + style: secondary + start: 0 + end: 25 + - source: myalias.Connection + style: secondary + start: 33 + end: 51 + - source: '''' + style: secondary + start: 73 + end: 74 + - source: '''' + style: secondary + start: 74 + end: 75 + - source: '''''' + style: secondary + start: 73 + end: 75 + - source: ('127.0.0.1', 'root', '', 'mail') + style: secondary + start: 51 + end: 84 + ? | + import MySQLdb as myalias + db = myalias.connect(host="host", user="username", passwd="", db="dbname") + : labels: + - source: myalias.connect(host="host", user="username", passwd="", db="dbname") + style: primary + start: 31 + end: 100 + - source: MySQLdb + style: secondary + start: 7 + end: 14 + - source: myalias + style: secondary + start: 18 + end: 25 + - source: MySQLdb as myalias + style: secondary + start: 7 + end: 25 + - source: import MySQLdb as myalias + style: secondary + start: 0 + end: 25 + - source: import MySQLdb as myalias + style: secondary + start: 0 + end: 25 + - source: myalias.connect + style: secondary + start: 31 + end: 46 + - source: passwd + style: secondary + start: 77 + end: 83 + - source: '"' + style: secondary + start: 84 + end: 85 + - source: '"' + style: secondary + start: 85 + end: 86 + - source: '""' + style: secondary + start: 84 + end: 86 + - source: passwd="" + style: secondary + start: 77 + end: 86 + - source: (host="host", user="username", passwd="", db="dbname") + style: secondary + start: 46 + end: 100 diff --git a/tests/python/python-mysql-empty-password-python-test.yml b/tests/python/python-mysql-empty-password-python-test.yml new file mode 100644 index 00000000..d440edd7 --- /dev/null +++ b/tests/python/python-mysql-empty-password-python-test.yml @@ -0,0 +1,13 @@ +id: python-mysql-empty-password-python +valid: + - | + import mysql.connector + conn = mysql.connector.connect(username="abcz", passwd="abc") +invalid: + - | + import mysql.connector + conn = mysql.connector.connect(username="abcz", passwd="") + - | + import mysql.connector as mysql123 + def my_function(): + mysql123.connect(host="localhost",user="root",passwd="",database="aaa") diff --git a/tests/python/python-mysql-hardcoded-secret-python-test.yml b/tests/python/python-mysql-hardcoded-secret-python-test.yml new file mode 100644 index 00000000..2100582b --- /dev/null +++ b/tests/python/python-mysql-hardcoded-secret-python-test.yml @@ -0,0 +1,13 @@ +id: python-mysql-hardcoded-secret-python +valid: + - | + import mysql.connector + conn = mysql.connector.connect(username="abcz", passwd="") +invalid: + - | + import mysql.connector + conn = mysql.connector.connect(username="abcz", passwd="abc") + - | + import mysql.connector as mysql123 + def my_function(): + mysql123.connect(host="localhost",user="root",passwd="abc",database="aaa") diff --git a/tests/python/python-mysqlclient-empty-password-python-test.yml b/tests/python/python-mysqlclient-empty-password-python-test.yml new file mode 100644 index 00000000..f40b36cb --- /dev/null +++ b/tests/python/python-mysqlclient-empty-password-python-test.yml @@ -0,0 +1,33 @@ +id: python-mysqlclient-empty-password-python +valid: + - | + from MySQLdb import _mysql as mysql + db = mysql.connect( + host=FLAGS.host, user=FLAGS.user, passwd="password", db=FLAGS.db + ) +invalid: + - | + from MySQLdb import _mysql as mysql + db = mysql.connect( + host=FLAGS.host, user=FLAGS.user, passwd="", db=FLAGS.db + ) + - | + from MySQLdb import _mysql as mysql + db = mysql.connect("MYSQL_HOST", "MYSQL_USER", "", "MYSQL_DATABASE") + - | + from MySQLdb import _mysql + db = MySQLdb._mysql.connect('', '', "", '') + - | + from MySQLdb import _mysql + db = _mysql.connect( + host=FLAGS.host, user=FLAGS.user, passwd="", db=FLAGS.db + ) + - | + import MySQLdb as myalias + db = myalias.connect(host="host", user="username", passwd="", db="dbname") + - | + import MySQLdb as myalias + conn = myalias.Connection('127.0.0.1', 'root', '', 'mail') + - | + import MySQLdb + db = MySQLdb.Connection(host="127.0.0.1", user="root", passwd="", db="business")