Preventing Arbitrary Code Execution in Python3.8 With Auditing
In Python 3.8+, support for system auditing hooks was added. System auditing uses two main functions:
sys.addaudithook
: accepts a callable to execute when auditing events are raised anywhere in the current Python interpreter.
sys.audit
: takes an event name and passes *args (related to current execution) to all hooks added by the above.
Part of the benefit of the above is that it let's us validate or block certain behavior in not only own own code, but also the standard library and dependencies.
Not counting bugs in underlying C libraries, security bugs in Python code tend to come from eval()/exec(), os.system(), subproccess.POpen() and others. We can use audit hooks to prevent execution of these that we do not approve of.
Example
Say we want to block all os.system usage in our run time:
import sys
class DisabledForSecurityReasons(Exception): pass
def block_sys(event, cmd):
if event == "system":
print(cmd)
raise DisabledForSecurityReasons
sys.addaudithook(block_sys)
If the above is executed anywhere, all usage of os.system() anywhere in the interpreter will be interrupted by DisabledForSecurityReasons exception.
If needed, cmd
could be parsed to allow or disallow certain commands (whitelisting would be the best approach).
Real World Example
Onionr is my project for decentralized private networking. Since one of the goals of the project is to enable anonymous communication, it is important that no network connections leak to the clearnet)
To enforce this and other policies, I created a new submodule with an ironic name: bigbrother.
Big Brother uses a series of 'ministries' to enforce
Big Brother monitors for socket.connect and other events:
def sys_hook_entrypoint(event, info):
if event == 'socket.connect':
ministry.ofcommunication.detect_socket_leaks(info)
elif event == 'exec':
# logs and block both exec and eval
ministry.ofexec.block_exec(event, info)
elif event == 'system':
ministry.ofexec.block_system(info)
As hooks cannot be called selectively based on event name, I think it is best to have a single hook wrap other functions.
This is the function wrapped by my hook called for socket connections:
import ipaddress
import logger
from onionrexceptions import NetworkLeak
def detect_socket_leaks(socket_event):
"""is called by the big brother broker whenever
a socket connection happens.
raises exception & logs if not to loopback
"""
ip_address = socket_event[1][0]
# validate is valid ip address (no hostname, etc)
# raises NetworkLeak if not
try:
ipaddress.ip_address(ip_address)
except ValueError:
logger.warn(f'Conn made to {ip_address} outside of Tor/similar')
raise \
NetworkLeak('Conn to host/non local IP, this is a privacy issue!')
# Validate that the IP is localhost ipv4
if not ip_address.startswith('127'):
logger.warn(f'Conn made to {ip_address} outside of Tor/similar')
raise NetworkLeak('Conn to non local IP, this is a privacy concern!')
This code ensures that a socket was created to only ipv4 localhost, which in the context of Onionr would be either a proxy (Tor) or the Onionr daemon.
Conclusion
Using sys auditing, we can make our Python run times somewhat more secure with generally acceptable speed trade offs.
My above examples and usage in Onionr are likely not perfect and are not meant to stop an intentionally malicious module. To take full advantage of sys.audit, one would need to audit quite a lot of events.