Repository: securesauce/precli (language: python)

Issue:
Add rule for insecure socket creation
**Is your feature request related to a problem? Please describe.**
Check for socket bind such as "0.0.0.0" or "" which would bind to all addresses.

**Describe the solution you'd like**

```
import socket

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(("", 0))
```

**Describe alternatives you've considered**
n/a

**Additional context**
Also do IPv6 equivalent.
https://cwe.mitre.org/data/definitions/1327.html

IP addresses should also not be hardcoded as they can change, except for maybe 127.0.0.1, although that also should be localhost.

These functions take an address parameter:

- [x] https://docs.python.org/3/library/socket.html#socket.create_server
  - #362
- [x] https://docs.python.org/3/library/socket.html#socket.socket.bind
  - #362
- [x] https://docs.python.org/3/library/socketserver.html#socketserver.TCPServer
  - #364
- [x] https://docs.python.org/3/library/socketserver.html#socketserver.UDPServer
  - #364
- [x] https://docs.python.org/3/library/socketserver.html#socketserver.ForkingTCPServer
  - #364
- [x] https://docs.python.org/3/library/socketserver.html#socketserver.ForkingUDPServer 
  - #364
- [x] https://docs.python.org/3/library/socketserver.html#socketserver.ThreadingTCPServer
  - #364
- [x] https://docs.python.org/3/library/socketserver.html#socketserver.ThreadingUDPServer
  - #364
- [x] https://docs.python.org/3/library/http.server.html#http.server.HTTPServer
  - #366
- [x] https://docs.python.org/3/library/http.server.html#http.server.ThreadingHTTPServer
  - #366
- [x] https://docs.python.org/3/library/xmlrpc.server.html#xmlrpc.server.SimpleXMLRPCServer
  - #368
- [x] https://docs.python.org/3/library/xmlrpc.server.html#xmlrpc.server.DocXMLRPCServer
  - #368

Love this idea? Give it a 👍. We prioritize fulfilling features with the most 👍.



Discussion / hints:
Could use regex to match IP address string and IPv6

Fix patch:
diff --git a/docs/rules.md b/docs/rules.md
index f3d1742b..7ff766da 100644
--- a/docs/rules.md
+++ b/docs/rules.md
@@ -42,3 +42,4 @@
 | PY028 | [secrets — weak token](rules/python/stdlib/secrets-weak-token.md) | Insufficient Token Length |
 | PY029 | [socket — unrestricted bind](rules/python/stdlib/socket-unrestricted-bind.md) | Binding to an Unrestricted IP Address in `socket` Module |
 | PY030 | [socketserver — unrestricted bind](rules/python/stdlib/socketserver-unrestricted-bind.md) | Binding to an Unrestricted IP Address in `socketserver` Module |
+| PY031 | [http — unrestricted bind](rules/python/stdlib/http-server-unrestricted-bind.md) | Binding to an Unrestricted IP Address in `http.server` Module |
diff --git a/docs/rules/python/stdlib/http_server-unrestricted-bind.md b/docs/rules/python/stdlib/http_server-unrestricted-bind.md
new file mode 100644
index 00000000..a5eb32e0
--- /dev/null
+++ b/docs/rules/python/stdlib/http_server-unrestricted-bind.md
@@ -0,0 +1,10 @@
+---
+id: PY031
+title: http — unrestricted bind
+hide_title: true
+pagination_prev: null
+pagination_next: null
+slug: /rules/PY031
+---
+
+::: precli.rules.python.stdlib.http_server_unrestricted_bind
diff --git a/precli/rules/python/stdlib/http_server_unrestricted_bind.py b/precli/rules/python/stdlib/http_server_unrestricted_bind.py
new file mode 100644
index 00000000..6ce53058
--- /dev/null
+++ b/precli/rules/python/stdlib/http_server_unrestricted_bind.py
@@ -0,0 +1,109 @@
+# Copyright 2024 Secure Saurce LLC
+r"""
+# Binding to an Unrestricted IP Address in `http.server` Module
+
+Sockets can be bound to the IPv4 address `0.0.0.0` or IPv6 equivalent of
+`::`, which configures the socket to listen for incoming connections on all
+network interfaces. While this can be intended in environments where
+services are meant to be publicly accessible, it can also introduce significant
+security risks if the service is not intended for public or wide network
+access.
+
+Binding a socket to `0.0.0.0` or `::` can unintentionally expose the
+application to the wider network or the internet, making it accessible from
+any interface. This exposure can lead to unauthorized access, data breaches,
+or exploitation of vulnerabilities within the application if the service is
+not adequately secured or if the binding is unintended. Restricting the socket
+to listen on specific interfaces limits the exposure and reduces the attack
+surface.
+
+## Example
+
+```python
+from http.server import BaseHTTPRequestHandler
+from http.server import HTTPServer
+
+
+def run(server_class: HTTPServer, handler_class: BaseHTTPRequestHandler):
+    server_address = ("", 8000)
+    httpd = server_class(server_address, handler_class)
+    httpd.serve_forever()
+```
+
+## Remediation
+
+All socket bindings MUST specify a specific network interface or localhost
+(127.0.0.1/localhost for IPv4, ::1 for IPv6) unless the application is
+explicitly designed to be accessible from any network interface. This
+practice ensures that services are not exposed more broadly than intended.
+
+```python
+from http.server import BaseHTTPRequestHandler
+from http.server import HTTPServer
+
+
+def run(server_class: HTTPServer, handler_class: BaseHTTPRequestHandler):
+    server_address = ("127.0.0.1", 8000)
+    httpd = server_class(server_address, handler_class)
+    httpd.serve_forever()
+```
+
+## See also
+
+- [http.server.HTTPServer — HTTP servers](https://docs.python.org/3/library/http.server.html#http.server.HTTPServer)
+- [http.server.ThreadingHTTPServer — HTTP servers](https://docs.python.org/3/library/http.server.html#http.server.ThreadingHTTPServer)
+- [CWE-1327: Binding to an Unrestricted IP Address](https://cwe.mitre.org/data/definitions/1327.html)
+
+_New in version 0.3.14_
+
+"""  # noqa: E501
+from precli.core.location import Location
+from precli.core.result import Result
+from precli.rules import Rule
+
+
+INADDR_ANY = "0.0.0.0"
+IN6ADDR_ANY = "::"
+
+
+class HttpServerUnrestrictedBind(Rule):
+    def __init__(self, id: str):
+        super().__init__(
+            id=id,
+            name="unrestricted_bind",
+            description=__doc__,
+            cwe_id=1327,
+            message="Binding to '{0}' exposes the application on all network "
+            "interfaces, increasing the risk of unauthorized access.",
+            targets=("call"),
+            wildcards={
+                "http.server.*": [
+                    "HTTPServer",
+                    "ThreadingHTTPServer",
+                ]
+            },
+        )
+
+    def analyze(self, context: dict, **kwargs: dict) -> Result:
+        call = kwargs.get("call")
+        if call.name_qualified not in [
+            "http.server.HTTPServer",
+            "http.server.ThreadingHTTPServer",
+        ]:
+            return
+
+        arg = call.get_argument(position=0, name="server_address")
+        server_address = arg.value
+
+        if isinstance(server_address, tuple) and server_address[0] in (
+            "",
+            INADDR_ANY,
+            IN6ADDR_ANY,
+        ):
+            return Result(
+                rule_id=self.id,
+                location=Location(node=arg.node),
+                message=self.message.format(
+                    "INADDR_ANY (0.0.0.0) or IN6ADDR_ANY (::)"
+                ),
+            )
diff --git a/setup.cfg b/setup.cfg
index 84bb62b4..8e43a4a0 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -136,5 +136,8 @@ precli.rules.python =
     # precli/rules/python/stdlib/socketserver_unrestricted_bind.py
     PY030 = precli.rules.python.stdlib.socketserver_unrestricted_bind:SocketserverUnrestrictedBind
 
+    # precli/rules/python/stdlib/http_server_unrestricted_bind.py
+    PY031 = precli.rules.python.stdlib.http_server_unrestricted_bind:HttpServerUnrestrictedBind
+
 [build_sphinx]
 all_files = 1


Test patch:
diff --git a/tests/unit/rules/python/stdlib/http/examples/http_server_http_server.py b/tests/unit/rules/python/stdlib/http/examples/http_server_http_server.py
new file mode 100644
index 00000000..db6eaa17
--- /dev/null
+++ b/tests/unit/rules/python/stdlib/http/examples/http_server_http_server.py
@@ -0,0 +1,13 @@
+# level: WARNING
+# start_line: 12
+# end_line: 12
+# start_column: 25
+# end_column: 39
+from http.server import BaseHTTPRequestHandler
+from http.server import HTTPServer
+
+
+def run(server_class: HTTPServer, handler_class: BaseHTTPRequestHandler):
+    server_address = ("", 8000)
+    httpd = server_class(server_address, handler_class)
+    httpd.serve_forever()
diff --git a/tests/unit/rules/python/stdlib/http/examples/http_server_threading_http_server.py b/tests/unit/rules/python/stdlib/http/examples/http_server_threading_http_server.py
new file mode 100644
index 00000000..83d4c4c4
--- /dev/null
+++ b/tests/unit/rules/python/stdlib/http/examples/http_server_threading_http_server.py
@@ -0,0 +1,16 @@
+# level: WARNING
+# start_line: 15
+# end_line: 15
+# start_column: 25
+# end_column: 39
+from http.server import BaseHTTPRequestHandler
+from http.server import ThreadingHTTPServer
+
+
+def run(
+    server_class: ThreadingHTTPServer,
+    handler_class: BaseHTTPRequestHandler,
+):
+    server_address = ("", 8000)
+    httpd = server_class(server_address, handler_class)
+    httpd.serve_forever()
diff --git a/tests/unit/rules/python/stdlib/http/test_http_server_unrestricted_bind.py b/tests/unit/rules/python/stdlib/http/test_http_server_unrestricted_bind.py
new file mode 100644
index 00000000..4d12e11c
--- /dev/null
+++ b/tests/unit/rules/python/stdlib/http/test_http_server_unrestricted_bind.py
@@ -0,0 +1,46 @@
+# Copyright 2024 Secure Saurce LLC
+import os
+
+from parameterized import parameterized
+
+from precli.core.level import Level
+from precli.parsers import python
+from precli.rules import Rule
+from tests.unit.rules import test_case
+
+
+class HttpServerUnrestrictedBindTests(test_case.TestCase):
+    def setUp(self):
+        super().setUp()
+        self.rule_id = "PY031"
+        self.parser = python.Python()
+        self.base_path = os.path.join(
+            "tests",
+            "unit",
+            "rules",
+            "python",
+            "stdlib",
+            "http",
+            "examples",
+        )
+
+    def test_rule_meta(self):
+        rule = Rule.get_by_id(self.rule_id)
+        self.assertEqual(self.rule_id, rule.id)
+        self.assertEqual("unrestricted_bind", rule.name)
+        self.assertEqual(
+            f"https://docs.securesauce.dev/rules/{self.rule_id}", rule.help_url
+        )
+        self.assertEqual(True, rule.default_config.enabled)
+        self.assertEqual(Level.WARNING, rule.default_config.level)
+        self.assertEqual(-1.0, rule.default_config.rank)
+        self.assertEqual("1327", rule.cwe.cwe_id)
+
+    @parameterized.expand(
+        [
+            "http_server_http_server.py",
+            "http_server_threading_http_server.py",
+        ]
+    )
+    def test(self, filename):
+        self.check(filename)