PyExtraSafe

GitHub Workflow Status Documentation Status PyPI Python >= 3.7 OS: Linux License

PyExtraSafe is a library that makes it easy to improve your program’s security by selectively allowing the syscalls it can perform via the Linux kernel’s seccomp facilities.

The Python library is a shallow wrapper around extrasafe.

Quick Example

from threading import Thread
import pyextrasafe

try:
    thread = Thread(target=print, args=["Hello, world!"])
    thread.start()
    thread.join()
except Exception:
    print("Could not run Thread (should have been able!)")

pyextrasafe.SafetyContext().enable(
    pyextrasafe.BasicCapabilities(),
    pyextrasafe.SystemIO().allow_stdout().allow_stderr(),
).apply_to_all_threads()

try:
    thread = Thread(target=print, args=["Hello, world!"])
    thread.start()
    thread.join()
except Exception:
    print("Could not run Thread (that's good!)")
else:
    raise Exception("Should not have been able to run thread")

Classes

final class SafetyContext

A struct representing a set of rules to be loaded into a seccomp filter and applied to the current thread, or all threads in the current process.

The seccomp filters will not be loaded until either apply_to_current_thread() or apply_to_all_threads() is called.

See also

Struct extrasafe::SafetyContext

enable(*policies: list[RuleSet]) SafetyContext

Enable the simple and conditional rules provided by the RuleSet.

Parameters:
*policies: list[RuleSet]

RuleSets to enable.

Returns:

This self object itself, so enable() can be chained.

Raises:

TypeError – Argument was not an instance of RuleSet.

apply_to_current_thread() None

Load the SafetyContext’s rules into a seccomp filter and apply the filter to the current thread.

Raises:

ExtraSafeError – Could not apply policies.

apply_to_all_threads() None

Load the SafetyContext()’s rules into a seccomp filter and apply the filter to all threads in this process.

Raises:

ExtraSafeError – Could not apply policies.

class RuleSet

A RuleSet is a collection of seccomp rules that enable a functionality.

exception ExtraSafeError

An exception thrown by PyExtraSafe.

Built-in profiles

All built-in profiles inherit from RuleSet. All methods return self, so calls can be chained.

digraph inheritance83e19bab87 { bgcolor=transparent; fontsize=32; rankdir=LR; size="6.0, 8.0"; "BasicCapabilities" [color="#ef5552",fillcolor=white,fontcolor="#000000de",fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="filled,solid"]; "RuleSet" -> "BasicCapabilities" [arrowsize=0.8,color="#ef5552",penwidth=1.2,style="setlinewidth(0.5)"]; "ForkAndExec" [color="#ef5552",fillcolor=white,fontcolor="#000000de",fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="filled,solid"]; "RuleSet" -> "ForkAndExec" [arrowsize=0.8,color="#ef5552",penwidth=1.2,style="setlinewidth(0.5)"]; "Networking" [color="#ef5552",fillcolor=white,fontcolor="#000000de",fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="filled,solid"]; "RuleSet" -> "Networking" [arrowsize=0.8,color="#ef5552",penwidth=1.2,style="setlinewidth(0.5)"]; "RuleSet" [color="#ef5552",fillcolor=white,fontcolor="#000000de",fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="filled,solid"]; "SystemIO" [color="#ef5552",fillcolor=white,fontcolor="#000000de",fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="filled,solid"]; "RuleSet" -> "SystemIO" [arrowsize=0.8,color="#ef5552",penwidth=1.2,style="setlinewidth(0.5)"]; "Threads" [color="#ef5552",fillcolor=white,fontcolor="#000000de",fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="filled,solid"]; "RuleSet" -> "Threads" [arrowsize=0.8,color="#ef5552",penwidth=1.2,style="setlinewidth(0.5)"]; "Time" [color="#ef5552",fillcolor=white,fontcolor="#000000de",fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="filled,solid"]; "RuleSet" -> "Time" [arrowsize=0.8,color="#ef5552",penwidth=1.2,style="setlinewidth(0.5)"]; }

pyextrasafe.Custom

final class BasicCapabilities

A RuleSet allowing basic required syscalls to do things like allocate memory, and also a few that are used by Rust to set up panic handling and segfault handlers.

final class ForkAndExec

ForkAndExec is in the danger zone because it can be used to start another process, including more privileged ones. That process will still be under seccomp’s restrictions but depending on your filter it could still do bad things.

final class Networking

A RuleSet representing syscalls that perform network operations - accept/listen/bind/connect etc.

By default, allow no networking syscalls.

allow_running_tcp_clients() Networking

Allow a running TCP client to continue running. Does not allow socket or connect to prevent new sockets from being created.

allow_running_tcp_servers() Networking

Allow a running TCP server to continue running. Does not allow socket or bind to prevent new sockets from being created.

allow_running_udp_sockets() Networking

Allow a running UDP socket to continue running. Does not allow socket or bind to prevent new sockets from being created.

allow_running_unix_clients() Networking

Allow a running Unix socket client to continue running. Does not allow socket or connect to prevent new sockets from being created.

allow_running_unix_servers() Networking

Allow a running Unix server to continue running. Does not allow socket or bind to prevent new sockets from being created.

allow_start_tcp_clients() Networking

Allow starting new TCP clients.

Warning

In some cases you can create the socket ahead of time, but in case it is not, we allow socket but not bind here.

allow_start_tcp_servers() Networking

Allow starting new TCP servers.

Warning

You probably don’t need to use this. In most cases you can just run your server and then use allow_running_tcp_servers().

allow_start_udp_servers() Networking

Allow starting new UDP sockets.

Warning

You probably don’t need to use this. In most cases you can just run your server and then use allow_running_udp_sockets().

allow_start_unix_servers() Networking

Allow starting new Unix domain servers

Warning

You probably don’t need to use this. In most cases you can just run your server and then use allow_running_unix_servers().

final class SystemIO

A RuleSet representing syscalls that perform IO - open/close/read/write/seek/stat.

By default, allow no IO syscalls.

static everything() SystemIO

Allow all IO syscalls.

allow_close() SystemIO

Allow close syscalls.

allow_ioctl() SystemIO

Allow ioctl and fcntl syscalls.

allow_metadata() SystemIO

Allow stat syscalls.

allow_open() SystemIO

Allow open syscalls.

allow_open_readonly() SystemIO

Allow open syscalls but not with write flags.

Note

Without this ruleset your program most likely won’t work, because Python won’t be able to read any modules that are not loaded, yet.

allow_read() SystemIO

Allow read syscalls.

allow_stderr() SystemIO

Allow writing to stderr.

allow_stdin() SystemIO

Allow reading from stdin.

allow_stdout() SystemIO

Allow writing to stdout.

allow_write() SystemIO

Allow write syscalls.

allow_file_read(fileno: int) SystemIO

Allow reading a given open file descriptor.

Warning

If another file or socket is opened after the file provided to this function is closed, it’s possible that the fd will be reused and therefore may be read from.

allow_file_write(fileno: int) SystemIO

Allow writing to a given open file descriptor.

Warning

If another file or socket is opened after the file provided to this function is closed, it’s possible that the fd will be reused and therefore may be read from.

final class Threads

Allows clone and sleep syscalls, which allow creating new threads and processes, and pausing them.

A new Threads ruleset allows nothing by default.

allow_create() Threads

Allow creating new threads and processes.

allow_sleep() Threads

Allow sleeping on the current thread

Warning

An attacker with arbitrary code execution and access to a high resolution timer can mount timing attacks (e.g. spectre).

final class Time

Enable syscalls related to time.

A new Time RuleSet allows nothing by default.

allow_gettime() Time

On most 64 bit systems glibc and musl both use the vDSO to compute the time directly with rdtsc rather than calling the clock_gettime syscall, so in most cases you don’t need to actually enable this.

Helper functions

These functions are not part of extrasafe, but they might come in handy anyways.

lock_pid_filelock_pid_file(path: str | os.PathLike, *, closefd: bool = False, cloexec: bool = True, mode: int = 0o640, contents: bytes | None = None) BinaryIO

Open and file-lock a PID file to prevent running multiple instances of a program.

If the PID file was non-existent, then a new file is created.

Parameters:
path: str | os.PathLike

The path of the PID file.

closefd: bool = False

By default (unless the function is called with closefd=True) the file descriptor of the opened PID file will leak if the returned File is collected, so the lock will be held until the process terminates.

cloexec: bool = True

By default the file descriptor will not be passed to sub processes. To pass the file descriptor to subprocesses use cloexec=False.

If you want to keep the file-lock as long as a subprocess is around, then you should probably still not use this flag, but os.dup() the file descriptor in Popen's preexec_fn parameter.

mode: int = 0o640

The file mode of the PID file. Only used if the file is newly created. If you supply a mode that is not readable and writable to the user, then all subsequent calls to this function will fail, whether the lock is still help or not. So make sure to always include 0o600 in the mode!

By default (0o640) the file will be readable and writable for its user; readable for the user’s group; and inaccessible for other users.

contents: bytes | None = None

By default the file will contain the PID of the current process followed by a newline.

Returns:

The opened file descriptor that holds the file lock.

Raises:

ExtraSafeError – If the file already existed, and a lock was held by another process, then the call will raise an exception.

restrict_privileges()

Basic security setup to prevent bootstrapping attacks.