Skip to content

Commit

Permalink
New Python rule checks use of Telnet with no timeout
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
ericwb committed Sep 29, 2024
1 parent 44d170f commit c6e6211
Show file tree
Hide file tree
Showing 12 changed files with 228 additions and 3 deletions.
2 changes: 2 additions & 0 deletions docs/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
10 changes: 10 additions & 0 deletions docs/rules/python/stdlib/poplib-no-timeout.md
Original file line number Diff line number Diff line change
@@ -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
10 changes: 10 additions & 0 deletions docs/rules/python/stdlib/telnetlib-no-timeout.md
Original file line number Diff line number Diff line change
@@ -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
127 changes: 127 additions & 0 deletions precli/rules/python/stdlib/telnetlib_no_timeout.py
Original file line number Diff line number Diff line change
@@ -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,
)
3 changes: 3 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# level: NONE
import telnetlib


telnet = telnetlib.Telnet("example.com", 23, 5)
Original file line number Diff line number Diff line change
@@ -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])

0 comments on commit c6e6211

Please sign in to comment.