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..6f445915 --- /dev/null +++ b/rules/python/security/python-mysqlclient-empty-password-python.yml @@ -0,0 +1,201 @@ +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 + +ast-grep-essentials: true + +utils: + define_string: + kind: string + all: + - has: + kind: string_start + nthChild: 1 + - has: + kind: string_end + nthChild: 2 + - not: + has: + kind: string_content + + define_password: + any: + - matches: define_string + - kind: identifier + pattern: $PWD_IDENTIFIER + inside: + stopBy: end + follows: + stopBy: end + kind: expression_statement + has: + stopBy: end + kind: assignment + nthChild: 1 + all: + - has: + nthChild: 1 + kind: identifier + field: left + pattern: $PWD_IDENTIFIER + - has: + nthChild: 2 + matches: define_string + + keyword_argument_passwd: + kind: keyword_argument + all: + - has: + nthChild: 1 + kind: identifier + field: name + regex: ^(passwd)$ + - has: + nthChild: 2 + matches: define_password + + argument_list_util: + kind: argument_list + any: + - has: + matches: keyword_argument_passwd + - all: + - has: + nthChild: + position: 3 + ofRule: + not: + kind: comment + matches: define_password + - not: + has: + matches: keyword_argument_passwd +rule: + any: + # MySQLdb.$CONNECT + - kind: call + any: + - kind: call + has: + nthChild: 1 + kind: attribute + all: + - has: + nthChild: 1 + kind: identifier + field: object + regex: ^MySQLdb$ + - has: + nthChild: 2 + kind: identifier + field: attribute + pattern: $CONNECT + precedes: + matches: argument_list_util + + # MySQLdb._mysql.$CONNECT + - kind: call + any: + - kind: call + has: + nthChild: 1 + kind: attribute + all: + - has: + nthChild: 1 + regex: ^MySQLdb._mysql$ + - has: + nthChild: 2 + kind: identifier + field: attribute + pattern: $CONNECT + precedes: + matches: argument_list_util + - kind: call + any: + - kind: call + has: + nthChild: 1 + kind: attribute + all: + - has: + nthChild: 1 + kind: identifier + field: object + regex: ^_mysql$ + - has: + nthChild: 2 + kind: identifier + field: attribute + pattern: $CONNECT + precedes: + matches: argument_list_util + inside: + stopBy: end + follows: + stopBy: end + kind: import_from_statement + has: + nthChild: 1 + kind: dotted_name + field: module_name + regex: ^MySQLdb$ + precedes: + stopBy: end + kind: dotted_name + regex: ^(_mysql)$ + + - kind: call + any: + - kind: call + has: + nthChild: 1 + kind: attribute + all: + - has: + nthChild: 1 + kind: identifier + field: object + pattern: $MYSQL_ALIAS + - has: + nthChild: 2 + kind: identifier + field: attribute + pattern: $CONNECT + precedes: + matches: argument_list_util + inside: + stopBy: end + follows: + stopBy: end + kind: import_from_statement + has: + nthChild: 1 + kind: dotted_name + field: module_name + regex: ^MySQLdb$ + precedes: + stopBy: end + kind: aliased_import + all: + - has: + nthChild: 1 + kind: dotted_name + field: name + regex: ^_mysql$ + - has: + nthChild: 2 + kind: identifier + field: alias + pattern: $MYSQL_ALIAS +# constraints: +# CONNECT: +# regex: ^(Connect|connect|Connection|connection)$ + diff --git a/rules/python/security/python-mysqlclient-hardcoded-secret-python.yml b/rules/python/security/python-mysqlclient-hardcoded-secret-python.yml new file mode 100644 index 00000000..a6943792 --- /dev/null +++ b/rules/python/security/python-mysqlclient-hardcoded-secret-python.yml @@ -0,0 +1,200 @@ +id: python-mysqlclient-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 + +ast-grep-essentials: true + +utils: + define_string: + kind: string + all: + - has: + kind: string_start + nthChild: 1 + - has: + kind: string_content + nthChild: 2 + - has: + kind: string_end + nthChild: 3 + + define_password: + any: + - matches: define_string + - kind: identifier + pattern: $PWD_IDENTIFIER + inside: + stopBy: end + follows: + stopBy: end + kind: expression_statement + has: + stopBy: end + kind: assignment + nthChild: 1 + all: + - has: + nthChild: 1 + kind: identifier + field: left + pattern: $PWD_IDENTIFIER + - has: + nthChild: 2 + matches: define_string + + keyword_argument_passwd: + kind: keyword_argument + all: + - has: + nthChild: 1 + kind: identifier + field: name + regex: ^(passwd)$ + - has: + nthChild: 2 + matches: define_password + + argument_list_util: + kind: argument_list + any: + - has: + matches: keyword_argument_passwd + - all: + - has: + nthChild: + position: 3 + ofRule: + not: + kind: comment + matches: define_password + - not: + has: + matches: keyword_argument_passwd +rule: + any: + # MySQLdb.$CONNECT + - kind: call + any: + - kind: call + has: + nthChild: 1 + kind: attribute + all: + - has: + nthChild: 1 + kind: identifier + field: object + regex: ^MySQLdb$ + - has: + nthChild: 2 + kind: identifier + field: attribute + pattern: $CONNECT + precedes: + matches: argument_list_util + + # MySQLdb._mysql.$CONNECT + - kind: call + any: + - kind: call + has: + nthChild: 1 + kind: attribute + all: + - has: + nthChild: 1 + regex: ^MySQLdb._mysql$ + - has: + nthChild: 2 + kind: identifier + field: attribute + pattern: $CONNECT + precedes: + matches: argument_list_util + - kind: call + any: + - kind: call + has: + nthChild: 1 + kind: attribute + all: + - has: + nthChild: 1 + kind: identifier + field: object + regex: ^_mysql$ + - has: + nthChild: 2 + kind: identifier + field: attribute + pattern: $CONNECT + precedes: + matches: argument_list_util + inside: + stopBy: end + follows: + stopBy: end + kind: import_from_statement + has: + nthChild: 1 + kind: dotted_name + field: module_name + regex: ^MySQLdb$ + precedes: + stopBy: end + kind: dotted_name + regex: ^(_mysql)$ + + - kind: call + any: + - kind: call + has: + nthChild: 1 + kind: attribute + all: + - has: + nthChild: 1 + kind: identifier + field: object + pattern: $MYSQL_ALIAS + - has: + nthChild: 2 + kind: identifier + field: attribute + pattern: $CONNECT + precedes: + matches: argument_list_util + inside: + stopBy: end + follows: + stopBy: end + kind: import_from_statement + has: + nthChild: 1 + kind: dotted_name + field: module_name + regex: ^MySQLdb$ + precedes: + stopBy: end + kind: aliased_import + all: + - has: + nthChild: 1 + kind: dotted_name + field: name + regex: ^_mysql$ + - has: + nthChild: 2 + kind: identifier + field: alias + pattern: $MYSQL_ALIAS +constraints: + CONNECT: + regex: ^(Connect|connect|Connection|connection)$ 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..c54cdb1c --- /dev/null +++ b/tests/__snapshots__/python-mysqlclient-empty-password-python-snapshot.yml @@ -0,0 +1,286 @@ +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 + style: secondary + start: 32 + end: 46 + - source: connect + style: secondary + start: 47 + 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 + - source: MySQLdb._mysql.connect + style: secondary + start: 32 + end: 54 + ? | + 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: 108 + - source: _mysql + style: secondary + start: 20 + end: 26 + - source: MySQLdb + style: secondary + start: 5 + end: 12 + - source: from MySQLdb import _mysql + style: secondary + start: 0 + end: 26 + - source: from MySQLdb import _mysql + style: secondary + start: 0 + end: 26 + - source: _mysql + style: secondary + start: 32 + end: 38 + - source: connect + style: secondary + start: 39 + end: 46 + - source: passwd + style: secondary + start: 84 + end: 90 + - source: '"' + style: secondary + start: 91 + end: 92 + - source: '"' + style: secondary + start: 92 + end: 93 + - source: '""' + style: secondary + start: 91 + end: 93 + - source: passwd="" + style: secondary + start: 84 + end: 93 + - source: |- + ( + host=FLAGS.host, user=FLAGS.user, passwd="", db=FLAGS.db + ) + style: secondary + start: 46 + end: 108 + - source: _mysql.connect + style: secondary + start: 32 + end: 46 + ? | + 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: _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: MySQLdb + style: secondary + start: 5 + end: 12 + - 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 + style: secondary + start: 41 + end: 46 + - source: connect + style: secondary + start: 47 + 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 + - source: mysql.connect + style: secondary + start: 41 + end: 54 + ? | + 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: _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: MySQLdb + style: secondary + start: 5 + end: 12 + - 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 + style: secondary + start: 41 + end: 46 + - source: connect + style: secondary + start: 47 + 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 + - source: mysql.connect + style: secondary + start: 41 + end: 54 + ? | + 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 + style: secondary + start: 20 + end: 27 + - source: Connection + style: secondary + start: 28 + 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 + - source: MySQLdb.Connection + style: secondary + start: 20 + end: 38 diff --git a/tests/__snapshots__/python-mysqlclient-hardcoded-secret-python-snapshot.yml b/tests/__snapshots__/python-mysqlclient-hardcoded-secret-python-snapshot.yml new file mode 100644 index 00000000..3d0f7efd --- /dev/null +++ b/tests/__snapshots__/python-mysqlclient-hardcoded-secret-python-snapshot.yml @@ -0,0 +1,306 @@ +id: python-mysqlclient-hardcoded-secret-python +snapshots: + ? | + from MySQLdb import _mysql + db = MySQLdb._mysql.connect('', '', "password", '') + : labels: + - source: MySQLdb._mysql.connect('', '', "password", '') + style: primary + start: 32 + end: 78 + - source: MySQLdb._mysql + style: secondary + start: 32 + end: 46 + - source: connect + style: secondary + start: 47 + end: 54 + - source: '"' + style: secondary + start: 63 + end: 64 + - source: password + style: secondary + start: 64 + end: 72 + - source: '"' + style: secondary + start: 72 + end: 73 + - source: '"password"' + style: secondary + start: 63 + end: 73 + - source: ('', '', "password", '') + style: secondary + start: 54 + end: 78 + - source: MySQLdb._mysql.connect + style: secondary + start: 32 + end: 54 + ? | + from MySQLdb import _mysql + db = _mysql.connect( + host=FLAGS.host, user=FLAGS.user, passwd="password", db=FLAGS.db + ) + : labels: + - source: |- + _mysql.connect( + host=FLAGS.host, user=FLAGS.user, passwd="password", db=FLAGS.db + ) + style: primary + start: 32 + end: 116 + - source: _mysql + style: secondary + start: 20 + end: 26 + - source: MySQLdb + style: secondary + start: 5 + end: 12 + - source: from MySQLdb import _mysql + style: secondary + start: 0 + end: 26 + - source: from MySQLdb import _mysql + style: secondary + start: 0 + end: 26 + - source: _mysql + style: secondary + start: 32 + end: 38 + - source: connect + style: secondary + start: 39 + end: 46 + - source: passwd + style: secondary + start: 84 + end: 90 + - source: '"' + style: secondary + start: 91 + end: 92 + - source: password + style: secondary + start: 92 + end: 100 + - source: '"' + style: secondary + start: 100 + end: 101 + - source: '"password"' + style: secondary + start: 91 + end: 101 + - source: passwd="password" + style: secondary + start: 84 + end: 101 + - source: |- + ( + host=FLAGS.host, user=FLAGS.user, passwd="password", db=FLAGS.db + ) + style: secondary + start: 46 + end: 116 + - source: _mysql.connect + style: secondary + start: 32 + end: 46 + ? | + from MySQLdb import _mysql as mysql + db = mysql.connect( + host=FLAGS.host, user=FLAGS.user, passwd="password", db=FLAGS.db + ) + : labels: + - source: |- + mysql.connect( + host=FLAGS.host, user=FLAGS.user, passwd="password", db=FLAGS.db + ) + style: primary + start: 41 + end: 124 + - 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: MySQLdb + style: secondary + start: 5 + end: 12 + - 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 + style: secondary + start: 41 + end: 46 + - source: connect + style: secondary + start: 47 + end: 54 + - source: passwd + style: secondary + start: 92 + end: 98 + - source: '"' + style: secondary + start: 99 + end: 100 + - source: password + style: secondary + start: 100 + end: 108 + - source: '"' + style: secondary + start: 108 + end: 109 + - source: '"password"' + style: secondary + start: 99 + end: 109 + - source: passwd="password" + style: secondary + start: 92 + end: 109 + - source: |- + ( + host=FLAGS.host, user=FLAGS.user, passwd="password", db=FLAGS.db + ) + style: secondary + start: 54 + end: 124 + - source: mysql.connect + style: secondary + start: 41 + end: 54 + ? | + from MySQLdb import _mysql as mysql + db = mysql.connect("MYSQL_HOST", "MYSQL_USER", "password", "MYSQL_DATABASE") + : labels: + - source: mysql.connect("MYSQL_HOST", "MYSQL_USER", "password", "MYSQL_DATABASE") + style: primary + start: 41 + end: 112 + - 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: MySQLdb + style: secondary + start: 5 + end: 12 + - 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 + style: secondary + start: 41 + end: 46 + - source: connect + style: secondary + start: 47 + end: 54 + - source: '"' + style: secondary + start: 83 + end: 84 + - source: password + style: secondary + start: 84 + end: 92 + - source: '"' + style: secondary + start: 92 + end: 93 + - source: '"password"' + style: secondary + start: 83 + end: 93 + - source: ("MYSQL_HOST", "MYSQL_USER", "password", "MYSQL_DATABASE") + style: secondary + start: 54 + end: 112 + - source: mysql.connect + style: secondary + start: 41 + end: 54 + ? | + import MySQLdb + db = MySQLdb.Connection(host="127.0.0.1", user="root", passwd="password", db="business") + : labels: + - source: MySQLdb.Connection(host="127.0.0.1", user="root", passwd="password", db="business") + style: primary + start: 20 + end: 103 + - source: MySQLdb + style: secondary + start: 20 + end: 27 + - source: Connection + style: secondary + start: 28 + end: 38 + - source: passwd + style: secondary + start: 70 + end: 76 + - source: '"' + style: secondary + start: 77 + end: 78 + - source: password + style: secondary + start: 78 + end: 86 + - source: '"' + style: secondary + start: 86 + end: 87 + - source: '"password"' + style: secondary + start: 77 + end: 87 + - source: passwd="password" + style: secondary + start: 70 + end: 87 + - source: (host="127.0.0.1", user="root", passwd="password", db="business") + style: secondary + start: 38 + end: 103 + - source: MySQLdb.Connection + style: secondary + start: 20 + end: 38 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..23ee64df --- /dev/null +++ b/tests/python/python-mysqlclient-empty-password-python-test.yml @@ -0,0 +1,27 @@ +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 + db = MySQLdb.Connection(host="127.0.0.1", user="root", passwd="", db="business") diff --git a/tests/python/python-mysqlclient-hardcoded-secret-python-test.yml b/tests/python/python-mysqlclient-hardcoded-secret-python-test.yml new file mode 100644 index 00000000..b2677762 --- /dev/null +++ b/tests/python/python-mysqlclient-hardcoded-secret-python-test.yml @@ -0,0 +1,27 @@ +id: python-mysqlclient-hardcoded-secret-python +valid: + - | + from MySQLdb import _mysql as mysql + db = mysql.connect( + host=FLAGS.host, user=FLAGS.user, passwd="", db=FLAGS.db + ) +invalid: + - | + from MySQLdb import _mysql as mysql + db = mysql.connect( + host=FLAGS.host, user=FLAGS.user, passwd="password", db=FLAGS.db + ) + - | + from MySQLdb import _mysql as mysql + db = mysql.connect("MYSQL_HOST", "MYSQL_USER", "password", "MYSQL_DATABASE") + - | + from MySQLdb import _mysql + db = MySQLdb._mysql.connect('', '', "password", '') + - | + from MySQLdb import _mysql + db = _mysql.connect( + host=FLAGS.host, user=FLAGS.user, passwd="password", db=FLAGS.db + ) + - | + import MySQLdb + db = MySQLdb.Connection(host="127.0.0.1", user="root", passwd="password", db="business")