From c6e6211e20e07f2409f039d60281d708215cedb3 Mon Sep 17 00:00:00 2001 From: Eric Brown Date: Sat, 28 Sep 2024 20:59:38 -0700 Subject: [PATCH] New Python rule checks use of Telnet with no timeout PY044 The Telnet class and open function on the object has timeout arguments that if undefined or set to None tell the library to use the global default timeout which has a default of None which has the effect of blocking forever. Signed-off-by: Eric Brown --- docs/rules.md | 2 + docs/rules/python/stdlib/poplib-no-timeout.md | 10 ++ .../python/stdlib/telnetlib-no-timeout.md | 10 ++ .../python/stdlib/telnetlib_no_timeout.py | 127 ++++++++++++++++++ setup.cfg | 3 + .../stdlib/telnetlib/examples/telnet.py | 2 +- .../telnetlib/examples/telnetlib_telnet.py | 2 +- .../examples/telnetlib_telnet_context_mgr.py | 2 +- .../examples/telnetlib_telnet_no_timeout.py | 9 ++ .../telnetlib_telnet_open_timeout_none.py | 10 ++ .../examples/telnetlib_telnet_timeout_5.py | 5 + .../telnetlib/test_telnetlib_no_timeout.py | 49 +++++++ 12 files changed, 228 insertions(+), 3 deletions(-) create mode 100644 docs/rules/python/stdlib/poplib-no-timeout.md create mode 100644 docs/rules/python/stdlib/telnetlib-no-timeout.md create mode 100644 precli/rules/python/stdlib/telnetlib_no_timeout.py create mode 100644 tests/unit/rules/python/stdlib/telnetlib/examples/telnetlib_telnet_no_timeout.py create mode 100644 tests/unit/rules/python/stdlib/telnetlib/examples/telnetlib_telnet_open_timeout_none.py create mode 100644 tests/unit/rules/python/stdlib/telnetlib/examples/telnetlib_telnet_timeout_5.py create mode 100644 tests/unit/rules/python/stdlib/telnetlib/test_telnetlib_no_timeout.py diff --git a/docs/rules.md b/docs/rules.md index 720ff00a..36f52bae 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -66,3 +66,5 @@ | PY040 | [smtplib — no timeout](rules/python/stdlib/smtplib-no-timeout.md) | Synchronous Access of `SMTP` without Timeout | | PY041 | [imaplib — no timeout](rules/python/stdlib/imaplib-no-timeout.md) | Synchronous Access of `IMAP4` without Timeout | | PY042 | [nntplib — no timeout](rules/python/stdlib/nntplib-no-timeout.md) | Synchronous Access of `NNTP` without Timeout | +| PY043 | [poplib — no timeout](rules/python/stdlib/poplib-no-timeout.md) | Synchronous Access of `POP3` without Timeout | +| PY044 | [telnetlib — no timeout](rules/python/stdlib/telnetlib-no-timeout.md) | Synchronous Access of `Telnet` without Timeout | diff --git a/docs/rules/python/stdlib/poplib-no-timeout.md b/docs/rules/python/stdlib/poplib-no-timeout.md new file mode 100644 index 00000000..e1e2b80d --- /dev/null +++ b/docs/rules/python/stdlib/poplib-no-timeout.md @@ -0,0 +1,10 @@ +--- +id: PY043 +title: poplib — no timeout +hide_title: true +pagination_prev: null +pagination_next: null +slug: /rules/PY043 +--- + +::: precli.rules.python.stdlib.poplib_no_timeout diff --git a/docs/rules/python/stdlib/telnetlib-no-timeout.md b/docs/rules/python/stdlib/telnetlib-no-timeout.md new file mode 100644 index 00000000..e8dd6784 --- /dev/null +++ b/docs/rules/python/stdlib/telnetlib-no-timeout.md @@ -0,0 +1,10 @@ +--- +id: PY044 +title: telnetlib — no timeout +hide_title: true +pagination_prev: null +pagination_next: null +slug: /rules/PY044 +--- + +::: precli.rules.python.stdlib.telnetlib_no_timeout diff --git a/precli/rules/python/stdlib/telnetlib_no_timeout.py b/precli/rules/python/stdlib/telnetlib_no_timeout.py new file mode 100644 index 00000000..b3162c59 --- /dev/null +++ b/precli/rules/python/stdlib/telnetlib_no_timeout.py @@ -0,0 +1,127 @@ +# Copyright 2024 Secure Sauce LLC +r""" +# Synchronous Access of `Telnet` without Timeout + +The `telnetlib.Telnet` class and the `telnetlib.Telnet.open()` method are +used to establish a connection to a remote server using the Telnet protocol. +By default, these operations do not enforce a timeout on the connection, +which can lead to indefinite blocking if the server is unresponsive. This +can result in resource exhaustion, application hanging, or Denial of Service +(DoS) vulnerabilities, especially in networked or production environments. + +This rule ensures that a timeout parameter is provided when using +`telnetlib.Telnet` and `telnetlib.Telnet.open()` to prevent the risk of +indefinite blocking during network communications. + +Failing to specify a timeout in these classes may cause the application to +block indefinitely while waiting for a response from the mail server. This can +lead to Denial of Service (DoS) vulnerabilities or cause the application to +become unresponsive. + +# Example + +```python linenums="1" hl_lines="4" title="telnetlib_telnet_no_timeout.py" +import telnetlib + + +telnet = telnetlib.Telnet("example.com", 23) +``` + +??? example "Example Output" + ``` + > precli tests/unit/rules/python/stdlib/telnetlib/examples/telnetlib_telnet_no_timeout.py + ⚠️ Warning on line 9 in tests/unit/rules/python/stdlib/telnetlib/examples/telnetlib_telnet_no_timeout.py + PY044: Synchronous Access of Remote Resource without Timeout + The class 'telnetlib.Telnet' is used without a timeout, which may cause the application to block indefinitely if the remote server does not respond. + ``` + +# Remediation + +Always provide a timeout parameter when using `telnetlib.Telnet` or +`telnetlib.Telnet.open()`. This ensures that if the mail server is unreachable +or unresponsive, the connection attempt will fail after a set period, +preventing indefinite blocking and resource exhaustion. + +```python linenums="1" hl_lines="4" title="telnetlib_telnet_no_timeout.py" +import telnetlib + + +telnet = telnetlib.Telnet("example.com", 23, timeout=5) +``` + +# See also + +!!! info + - [telnetlib.Telnet — telnetlib — Telnet client](https://docs.python.org/3/library/telnetlib.html#telnetlib.Telnet) + - [telnetlib.Telnet.open — telnetlib — Telnet client](https://docs.python.org/3/library/telnetlib.html#telnetlib.Telnet.open) + - [CWE-1088: Synchronous Access of Remote Resource without Timeout](https://cwe.mitre.org/data/definitions/1088.html) + +_New in version 0.6.7_ + +""" # noqa: E501 +from precli.core.call import Call +from precli.core.location import Location +from precli.core.result import Result +from precli.rules import Rule + + +class TelnetlibNoTimeout(Rule): + def __init__(self, id: str): + super().__init__( + id=id, + name="no_timeout", + description=__doc__, + cwe_id=1088, + message="The class '{0}' is used without a timeout, which may " + "cause the application to block indefinitely if the remote server " + "does not respond.", + ) + + def analyze_call(self, context: dict, call: Call) -> Result | None: + if call.name_qualified not in ( + "telnetlib.Telnet", + "telnetlib.Telnet.open", + ): + return + + if ( + call.name_qualified == "telnetlib.Telnet" + and call.get_argument(position=0, name="host").node is None + ): + return + + # Telnet(host=None, port=0, timeout=GLOBAL_TIMEOUT) + # Telnet.open(self, host, port=0, timeout=GLOBAL_TIMEOUT) + argument = call.get_argument(position=2, name="timeout") + timeout = argument.value + + if argument.node is None: + arg_list_node = call.arg_list_node + fix_node = arg_list_node + args = [child.string for child in arg_list_node.named_children] + args.append("timeout=5") + content = f"({', '.join(args)})" + result_node = call.arg_list_node + elif timeout is None: + fix_node = argument.node + result_node = argument.node + content = "5" + else: + # If the timeout parameter is set to be zero, the class will raise + # a ValueError to prevent the creation of a non-blocking socket. A + # negative value also raises ValueError. So there is no need to + # check for these values. + return + + fixes = Rule.get_fixes( + context=context, + deleted_location=Location(fix_node), + description="Set timeout parameter to a small number of seconds.", + inserted_content=content, + ) + return Result( + rule_id=self.id, + location=Location(node=result_node), + message=self.message.format(call.name_qualified), + fixes=fixes, + ) diff --git a/setup.cfg b/setup.cfg index e332f262..255954f9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -212,3 +212,6 @@ precli.rules.python = # precli/rules/python/stdlib/poplib_no_timeout.py PY043 = precli.rules.python.stdlib.poplib_no_timeout:PoplibNoTimeout + + # precli/rules/python/stdlib/telnetlib_no_timeout.py + PY044 = precli.rules.python.stdlib.telnetlib_no_timeout:TelnetlibNoTimeout diff --git a/tests/unit/rules/python/stdlib/telnetlib/examples/telnet.py b/tests/unit/rules/python/stdlib/telnetlib/examples/telnet.py index 3af03a1b..31d5f807 100644 --- a/tests/unit/rules/python/stdlib/telnetlib/examples/telnet.py +++ b/tests/unit/rules/python/stdlib/telnetlib/examples/telnet.py @@ -11,7 +11,7 @@ user = input("Enter your remote account: ") password = getpass.getpass() -tn = Telnet(HOST) +tn = Telnet(HOST, timeout=5) tn.read_until(b"login: ") tn.write(user.encode("ascii") + b"\n") diff --git a/tests/unit/rules/python/stdlib/telnetlib/examples/telnetlib_telnet.py b/tests/unit/rules/python/stdlib/telnetlib/examples/telnetlib_telnet.py index 85946a58..cb8f6d8b 100644 --- a/tests/unit/rules/python/stdlib/telnetlib/examples/telnetlib_telnet.py +++ b/tests/unit/rules/python/stdlib/telnetlib/examples/telnetlib_telnet.py @@ -11,7 +11,7 @@ user = input("Enter your remote account: ") password = getpass.getpass() -tn = telnetlib.Telnet(HOST) +tn = telnetlib.Telnet(HOST, timeout=5) tn.read_until(b"login: ") tn.write(user.encode("ascii") + b"\n") diff --git a/tests/unit/rules/python/stdlib/telnetlib/examples/telnetlib_telnet_context_mgr.py b/tests/unit/rules/python/stdlib/telnetlib/examples/telnetlib_telnet_context_mgr.py index fb7fb9ff..1ac3a175 100644 --- a/tests/unit/rules/python/stdlib/telnetlib/examples/telnetlib_telnet_context_mgr.py +++ b/tests/unit/rules/python/stdlib/telnetlib/examples/telnetlib_telnet_context_mgr.py @@ -11,7 +11,7 @@ user = input("Enter your remote account: ") password = getpass.getpass() -with telnetlib.Telnet(HOST) as tn: +with telnetlib.Telnet(HOST, timeout=5) as tn: tn.read_until(b"login: ") tn.write(user.encode("ascii") + b"\n") if password: diff --git a/tests/unit/rules/python/stdlib/telnetlib/examples/telnetlib_telnet_no_timeout.py b/tests/unit/rules/python/stdlib/telnetlib/examples/telnetlib_telnet_no_timeout.py new file mode 100644 index 00000000..758654e6 --- /dev/null +++ b/tests/unit/rules/python/stdlib/telnetlib/examples/telnetlib_telnet_no_timeout.py @@ -0,0 +1,9 @@ +# level: WARNING +# start_line: 9 +# end_line: 9 +# start_column: 25 +# end_column: 44 +import telnetlib + + +telnet = telnetlib.Telnet("example.com", 23) diff --git a/tests/unit/rules/python/stdlib/telnetlib/examples/telnetlib_telnet_open_timeout_none.py b/tests/unit/rules/python/stdlib/telnetlib/examples/telnetlib_telnet_open_timeout_none.py new file mode 100644 index 00000000..78e8fc3d --- /dev/null +++ b/tests/unit/rules/python/stdlib/telnetlib/examples/telnetlib_telnet_open_timeout_none.py @@ -0,0 +1,10 @@ +# level: WARNING +# start_line: 10 +# end_line: 10 +# start_column: 35 +# end_column: 39 +import telnetlib + + +telnet = telnetlib.Telnet() +telnet.open("example.com", timeout=None) diff --git a/tests/unit/rules/python/stdlib/telnetlib/examples/telnetlib_telnet_timeout_5.py b/tests/unit/rules/python/stdlib/telnetlib/examples/telnetlib_telnet_timeout_5.py new file mode 100644 index 00000000..2a828e8d --- /dev/null +++ b/tests/unit/rules/python/stdlib/telnetlib/examples/telnetlib_telnet_timeout_5.py @@ -0,0 +1,5 @@ +# level: NONE +import telnetlib + + +telnet = telnetlib.Telnet("example.com", 23, 5) diff --git a/tests/unit/rules/python/stdlib/telnetlib/test_telnetlib_no_timeout.py b/tests/unit/rules/python/stdlib/telnetlib/test_telnetlib_no_timeout.py new file mode 100644 index 00000000..a89111fc --- /dev/null +++ b/tests/unit/rules/python/stdlib/telnetlib/test_telnetlib_no_timeout.py @@ -0,0 +1,49 @@ +# Copyright 2024 Secure Sauce LLC +import os + +import pytest + +from precli.core.level import Level +from precli.parsers import python +from precli.rules import Rule +from tests.unit.rules import test_case + + +class TestTelnetlibNoTimeout(test_case.TestCase): + @classmethod + def setup_class(cls): + cls.rule_id = "PY044" + cls.parser = python.Python() + cls.base_path = os.path.join( + "tests", + "unit", + "rules", + "python", + "stdlib", + "telnetlib", + "examples", + ) + + def test_rule_meta(self): + rule = Rule.get_by_id(self.rule_id) + assert rule.id == self.rule_id + assert rule.name == "no_timeout" + assert ( + rule.help_url + == f"https://docs.securesauce.dev/rules/{self.rule_id}" + ) + assert rule.default_config.enabled is True + assert rule.default_config.level == Level.WARNING + assert rule.default_config.rank == -1.0 + assert rule.cwe.id == 1088 + + @pytest.mark.parametrize( + "filename", + [ + "telnetlib_telnet_no_timeout.py", + "telnetlib_telnet_open_timeout_none.py", + "telnetlib_telnet_timeout_5.py", + ], + ) + def test(self, filename): + self.check(filename, enabled=[self.rule_id])