Skip to content

Commit

Permalink
Add new rule for http.server unrestricted bind (#366)
Browse files Browse the repository at this point in the history
This checks for socket binds in the http.server module that use
unrestricted IP addresses ("0.0.0.0" or "::").

Partially implements #225

Signed-off-by: Eric Brown <[email protected]>
  • Loading branch information
ericwb authored Mar 18, 2024
1 parent ac680dc commit 0acd504
Show file tree
Hide file tree
Showing 7 changed files with 198 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
10 changes: 10 additions & 0 deletions docs/rules/python/stdlib/http_server-unrestricted-bind.md
Original file line number Diff line number Diff line change
@@ -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
109 changes: 109 additions & 0 deletions precli/rules/python/stdlib/http_server_unrestricted_bind.py
Original file line number Diff line number Diff line change
@@ -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 (::)"
),
)
3 changes: 3 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -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()
Original file line number Diff line number Diff line change
@@ -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()
Original file line number Diff line number Diff line change
@@ -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)

0 comments on commit 0acd504

Please sign in to comment.