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])