1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121
|
#!/usr/bin/env python3
"""
Graphviz sandbox
This program is a wrapper around Graphviz. It aims to provide a safe environment for the
processing of untrusted input graphs and command line options. More precisely:
1. No network access will be allowed.
2. The file system will be read-only. Command line options like `-o …` and `-O` will
not work. It is expected that the caller will render to stdout and pipe the output
to their desired file.
"""
import abc
import platform
import shutil
import subprocess as sp
import sys
from pathlib import Path
from typing import Optional, Type, Union
class Sandbox:
"""
API for sandbox interaction
Specific sandbox mechanisms should be implemented as derived classes of this.
"""
@staticmethod
@abc.abstractmethod
def is_usable() -> bool:
"""is this sandbox available on the current platform?"""
raise NotImplementedError
@staticmethod
@abc.abstractmethod
def _run(args: list[Union[Path, str]]) -> int:
"""run the given command line within the sandbox"""
raise NotImplementedError
@classmethod
def run(cls, args: list[Union[Path, str]]) -> int:
"""wrapper around `_run` to perform common sanity checks"""
assert cls.is_usable(), "attempted to use unusable sandbox"
return cls._run(args)
class Bubblewrap(Sandbox):
"""
Bubblewrap¹-based sandbox
¹ https://github.com/containers/bubblewrap
"""
@staticmethod
def is_usable() -> bool:
return shutil.which("bwrap") is not None
@staticmethod
def _run(args: list[Union[Path, str]]) -> sp.CompletedProcess:
prefix = ["bwrap", "--ro-bind", "/", "/", "--unshare-all", "--"]
return sp.call(prefix + args)
class MacSandbox(Sandbox):
"""
macOS’ sandbox API
"""
@staticmethod
def is_usable() -> bool:
return platform.system() == "Darwin"
@staticmethod
def _run(args: list[Union[Path, str]]) -> sp.CompletedProcess:
# create a sandbox policy that allows only read-only file access
policy = (
'(version 1)(allow default)(deny file-write* (subpath "/"))(deny network*)'
)
# run Graphviz under this policy
prefix = ["sandbox-exec", "-p", policy]
return sp.call(prefix + args)
def main(args: list[str]) -> int:
"""entry point"""
# available sandboxes in order of preference
SANDBOXES: tuple[Type[Sandbox]] = (Bubblewrap, MacSandbox)
# locate Graphviz, preferring the version collocated with us
exe = ".exe" if platform.system() == "Windows" else ""
dot = Path(__file__).parent / f"dot{exe}"
if not dot.exists():
dot = shutil.which("dot")
if dot is None:
sys.stderr.write("Graphviz (`dot`) not found\n")
return -1
# find a usable sandbox
sandbox: Optional[Type[Sandbox]] = None
for box in SANDBOXES:
if not box.is_usable():
continue
sandbox = box
break
if sandbox is None:
sys.stderr.write("no usable sandbox found\n")
return -1
dot_args = args[1:]
# run Graphviz
return sandbox.run([dot] + dot_args)
if __name__ == "__main__":
sys.exit(main(sys.argv))
|