From 8d7ab883ccf383965bee809afd5da4690139c625 Mon Sep 17 00:00:00 2001 From: clavedeluna Date: Mon, 12 Sep 2022 09:26:14 -0300 Subject: [PATCH 1/2] add basic input password linter --- dlint/linters/__init__.py | 2 ++ dlint/linters/input_password_ask.py | 33 ++++++++++++++++++++ docs/README.md | 1 + docs/linters/DUO139.md | 25 +++++++++++++++ tests/test_input_password_ask.py | 48 +++++++++++++++++++++++++++++ 5 files changed, 109 insertions(+) create mode 100644 dlint/linters/input_password_ask.py create mode 100644 docs/linters/DUO139.md create mode 100644 tests/test_input_password_ask.py diff --git a/dlint/linters/__init__.py b/dlint/linters/__init__.py index 2de74c9..eb032bd 100644 --- a/dlint/linters/__init__.py +++ b/dlint/linters/__init__.py @@ -36,6 +36,7 @@ from .bad_xmlsec_module_attribute_use import BadXmlsecModuleAttributeUseLinter from .bad_yaml_use import BadYAMLUseLinter from .bad_zipfile_use import BadZipfileUseLinter +from .input_password_ask import InputPasswordUseLinter from .twisted.inlinecallbacks_yield_statement import InlineCallbacksYieldStatementLinter from .twisted.returnvalue_in_inlinecallbacks import ReturnValueInInlineCallbacksLinter @@ -80,4 +81,5 @@ InlineCallbacksYieldStatementLinter, ReturnValueInInlineCallbacksLinter, YieldReturnStatementLinter, + InputPasswordUseLinter, ) diff --git a/dlint/linters/input_password_ask.py b/dlint/linters/input_password_ask.py new file mode 100644 index 0000000..9171a83 --- /dev/null +++ b/dlint/linters/input_password_ask.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python + +from . import base + + +class InputPasswordUseLinter(base.BaseLinter): + """This linter looks for use of the Python "input" function with a string + argument containing the word "password" in any format. + """ + off_by_default = False + + _code = 'DUO139' + _error_tmpl = 'DUO139 avoid using "input" to ask for password' + + def visit_Call(self, node): + + if (node.func.id == "input"): + value = self._get_arg_or_kwarg(node) + + if "password" in value.lower(): + self.results.append( + base.Flake8Result( + lineno=node.lineno, + col_offset=node.col_offset, + message=self._error_tmpl + ) + ) + + def _get_arg_or_kwarg(self, node): + if node.args: + return node.args[0].value + if node.keywords: + return node.args[0].value diff --git a/docs/README.md b/docs/README.md index 2ac97ea..cb054b8 100644 --- a/docs/README.md +++ b/docs/README.md @@ -42,6 +42,7 @@ Dlint uses a simple, folder-based hierarchy written in [Markdown](https://en.wik - [`DUO136` `BadXmlsecModuleAttributeUseLinter` insecure "xmlsec" attribute use](https://github.com/dlint-py/dlint/blob/master/docs/linters/DUO136.md) - [`DUO137` `BadItsDangerousKwargUseLinter` insecure "itsdangerous" use allowing empty signing](https://github.com/dlint-py/dlint/blob/master/docs/linters/DUO137.md) - [`DUO138` `BadReCatastrophicUseLinter` catastrophic "re" usage - denial-of-service possible](https://github.com/dlint-py/dlint/blob/master/docs/linters/DUO138.md) +- todo # FAQs diff --git a/docs/linters/DUO139.md b/docs/linters/DUO139.md new file mode 100644 index 0000000..65be27b --- /dev/null +++ b/docs/linters/DUO139.md @@ -0,0 +1,25 @@ +# DUO139 + +This linter searches for use of the built-in `input` function with some form of the string +"password" as an argument to the function. + +## Problematic code + +```python +input("password: ") +input("Password: ") +input("PASSWORD") +``` + +## Correct code + +```python +import getpass + +getpass.getpass() +``` + +## Rationale + +[getpass](https://docs.python.org/3.7/library/getpass.html) safely asks for a password without echoing the input by default. + diff --git a/tests/test_input_password_ask.py b/tests/test_input_password_ask.py new file mode 100644 index 0000000..b0d4eec --- /dev/null +++ b/tests/test_input_password_ask.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python + +import unittest + +import dlint + + +class TestInputPasswordUse(dlint.test.base.BaseTest): + + def test_no_password(self): + python_node = self.get_ast_node( + """ + input("enter your name:") + """ + ) + + linter = dlint.linters.InputPasswordUseLinter() + linter.visit(python_node) + + result = linter.get_results() + expected = [] + + assert result == expected + + def test_input_asks_password(self): + python_node = self.get_ast_node( + """ + input("enter your password:") + """ + ) + + linter = dlint.linters.InputPasswordUseLinter() + linter.visit(python_node) + + result = linter.get_results() + expected = [ + dlint.linters.base.Flake8Result( + lineno=2, + col_offset=0, + message=dlint.linters.InputPasswordUseLinter._error_tmpl + ) + ] + + assert result == expected + + +if __name__ == "__main__": + unittest.main() From 418d5752d28fda3cdbda840f64e1c47b254a5099 Mon Sep 17 00:00:00 2001 From: clavedeluna Date: Mon, 12 Sep 2022 09:40:21 -0300 Subject: [PATCH 2/2] test input password linter and allow for kwargs detection --- dlint/linters/input_password_ask.py | 2 +- tests/test_input_password_ask.py | 63 ++++++++++++++++------------- 2 files changed, 35 insertions(+), 30 deletions(-) diff --git a/dlint/linters/input_password_ask.py b/dlint/linters/input_password_ask.py index 9171a83..d10a1b3 100644 --- a/dlint/linters/input_password_ask.py +++ b/dlint/linters/input_password_ask.py @@ -30,4 +30,4 @@ def _get_arg_or_kwarg(self, node): if node.args: return node.args[0].value if node.keywords: - return node.args[0].value + return node.keywords[0].value.value diff --git a/tests/test_input_password_ask.py b/tests/test_input_password_ask.py index b0d4eec..4347577 100644 --- a/tests/test_input_password_ask.py +++ b/tests/test_input_password_ask.py @@ -1,47 +1,52 @@ #!/usr/bin/env python +import pytest import unittest import dlint -class TestInputPasswordUse(dlint.test.base.BaseTest): +@pytest.mark.parametrize("code", [ + """input('enter your name:')""", + """input = 1""", + """password = 'something'""", +]) +def test_input_password_not_used(code): + python_node = dlint.test.base.get_ast_node(code) - def test_no_password(self): - python_node = self.get_ast_node( - """ - input("enter your name:") - """ - ) + linter = dlint.linters.InputPasswordUseLinter() + linter.visit(python_node) - linter = dlint.linters.InputPasswordUseLinter() - linter.visit(python_node) + result = linter.get_results() + expected = [] - result = linter.get_results() - expected = [] + assert result == expected - assert result == expected - def test_input_asks_password(self): - python_node = self.get_ast_node( - """ - input("enter your password:") - """ - ) +@pytest.mark.parametrize("code", [ + """input('enter your password:')""", + """input('enter your PASSWORD:')""", + # unable to detect if the intent wasn't to ask for password because word 'password' is present + """input('Please enter your name. Please do not enter your password')""", + """input(prompt='enter your PASSWORD:')""", +]) +def test_input_password_bad(code): + python_node = dlint.test.base.get_ast_node(code) - linter = dlint.linters.InputPasswordUseLinter() - linter.visit(python_node) + linter = dlint.linters.InputPasswordUseLinter() + linter.visit(python_node) - result = linter.get_results() - expected = [ - dlint.linters.base.Flake8Result( - lineno=2, - col_offset=0, - message=dlint.linters.InputPasswordUseLinter._error_tmpl - ) - ] + result = linter.get_results() + + expected = [ + dlint.linters.base.Flake8Result( + lineno=1, + col_offset=0, + message=dlint.linters.InputPasswordUseLinter._error_tmpl + ) + ] - assert result == expected + assert result == expected if __name__ == "__main__":