-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add new rule for http.server unrestricted bind (#366)
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
Showing
7 changed files
with
198 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
109
precli/rules/python/stdlib/http_server_unrestricted_bind.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 (::)" | ||
), | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
13 changes: 13 additions & 0 deletions
13
tests/unit/rules/python/stdlib/http/examples/http_server_http_server.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
16 changes: 16 additions & 0 deletions
16
tests/unit/rules/python/stdlib/http/examples/http_server_threading_http_server.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
46 changes: 46 additions & 0 deletions
46
tests/unit/rules/python/stdlib/http/test_http_server_unrestricted_bind.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |