Skip to content

Commit

Permalink
Add rule for xmlrpc.server unrestricted bind (#368)
Browse files Browse the repository at this point in the history
Checks xmlrpc.server classes that also binds sockets with parameters
that allow unrestricted addresses (ANY) as input.

Closes: #225

Signed-off-by: Eric Brown <[email protected]>
  • Loading branch information
ericwb authored Mar 18, 2024
1 parent 1e3a28a commit bbf09e9
Show file tree
Hide file tree
Showing 8 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 @@ -43,3 +43,4 @@
| 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 |
| PY032 | [xmlrpc — unrestricted bind](rules/python/stdlib/xmlrpc-server-unrestricted-bind.md) | Binding to an Unrestricted IP Address in `xmlrpc.server` Module |
10 changes: 10 additions & 0 deletions docs/rules/python/stdlib/xmlrpc-server-unrestricted-bind.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
id: PY032
title: xmlrpc — unrestricted bind
hide_title: true
pagination_prev: null
pagination_next: null
slug: /rules/PY032
---

::: precli.rules.python.stdlib.xmlrpc_server_unrestricted_bind
109 changes: 109 additions & 0 deletions precli/rules/python/stdlib/xmlrpc_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 `xmlrpc.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
- [xmlrpc.server.DocXMLRPCServer — Basic XML-RPC servers](https://docs.python.org/3/library/xmlrpc.server.html#xmlrpc.server.DocXMLRPCServer)
- [xmlrpc.server.SimpleXMLRPCServer — HTTP servers](https://docs.python.org/3/library/xmlrpc.server.html#xmlrpc.server.SimpleXMLRPCServer)
- [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 XmlrpcServerUnrestrictedBind(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={
"xmlrpc.server.*": [
"DocXMLRPCServer",
"SimpleXMLRPCServer",
]
},
)

def analyze(self, context: dict, **kwargs: dict) -> Result:
call = kwargs.get("call")
if call.name_qualified not in [
"xmlrpc.server.DocXMLRPCServer",
"xmlrpc.server.SimpleXMLRPCServer",
]:
return

arg = call.get_argument(position=0, name="addr")
addr = arg.value

if isinstance(addr, tuple) and addr[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 @@ -139,5 +139,8 @@ precli.rules.python =
# precli/rules/python/stdlib/http_server_unrestricted_bind.py
PY031 = precli.rules.python.stdlib.http_server_unrestricted_bind:HttpServerUnrestrictedBind

# precli/rules/python/stdlib/xmlrpc_server_unrestricted_bind.py
PY032 = precli.rules.python.stdlib.xmlrpc_server_unrestricted_bind:XmlrpcServerUnrestrictedBind

[build_sphinx]
all_files = 1
Empty file.
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 xmlrpc.server import DocXMLRPCRequestHandler
from xmlrpc.server import DocXMLRPCServer


def run(server_class: DocXMLRPCServer, handler_class: DocXMLRPCRequestHandler):
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 xmlrpc.server import SimpleXMLRPCRequestHandler
from xmlrpc.server import SimpleXMLRPCServer


def run(
server_class: SimpleXMLRPCServer,
handler_class: SimpleXMLRPCRequestHandler,
):
server_address = ("0.0.0.0", 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 XmlrpcServerUnrestrictedBindTests(test_case.TestCase):
def setUp(self):
super().setUp()
self.rule_id = "PY032"
self.parser = python.Python()
self.base_path = os.path.join(
"tests",
"unit",
"rules",
"python",
"stdlib",
"xmlrpc",
"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(
[
"xmlrpc_server_doc_xml_rpc_server.py",
"xmlrpc_server_simple_xml_rpc_server.py",
]
)
def test(self, filename):
self.check(filename)

0 comments on commit bbf09e9

Please sign in to comment.