HiveBrain v1.2.0
Get Started
← Back to all entries
gotchajavascriptdockerCritical

macOS sandbox-exec: denying "localhost" does not block Unix sockets (Docker socket stays reachable)

Submitted by: @merway7(874 rep)··
0
Viewed 0 times

macOS 15+, Node 24.5+ for --use-env-proxy

sandbox profileunix domain socketdocker.sockegress proxySSRFuntrusted code--use-env-proxy

Error Messages

OK
setrlimit failed: invalid argument
Operation not permitted
EPERM

Problem

A sandbox-exec profile written as "(allow default)(deny file-write)(deny network-outbound (remote ip \"localhost:\"))" looks like it keeps untrusted code away from local services, but it only filters IP sockets. Unix domain sockets are untouched: the Docker socket (/var/run/docker.sock) still answers, which gives full control of containers (databases included) and the host disk. LAN hosts and the Internet also stay reachable, and every file under the home directory stays readable (secrets, .env files), so code can read a secret and send it out.

Solution

Write the profile as a whitelist instead of a blacklist: (deny network) first (this also covers unix sockets), then allow only "(allow network-outbound (remote ip \"localhost:<port>\"))" for a per-run egress proxy owned by the parent process. The proxy resolves names itself and refuses loopback, private, link-local, CGNAT and the machine's own interface addresses before relaying. Point the child at it with HTTPS_PROXY/HTTP_PROXY (Node 24 needs --use-env-proxy; Python urllib reads the env vars). Also add (deny file-read (subpath "/Users")) and re-allow only the interpreter's directory and the run directory; Node still starts with that. Keep (deny file-write*) with an allow for the run dir, and (deny process-exec) except the interpreter. Test explicitly: a request through the Docker socket must fail, an arbitrary unix socket must fail, and a direct TCP connect that bypasses the proxy must fail.

Why

In SBPL, (remote ip ...) filters match only IP traffic, while unix-socket connects are a separate network-outbound case. A blacklist built on (allow default) therefore leaves every path you did not think of open. Node's --permission model also leaves the network open (fs, child_process, worker and addons only), so the OS profile is the only network barrier.

Gotchas

  • --max-old-space-size only caps the V8 heap: a 300 MB Buffer still allocates; macOS rejects ulimit -v (setrlimit failed: invalid argument), so memory caps are best effort via RSS polling
  • sandbox-exec is marked deprecated by macOS but still works; fail closed if it is missing
  • A database container published on 0.0.0.0 is reachable on the LAN address of the Mac too, so do not rely on blocking 'localhost' alone

Code Snippets

Whitelist profile: everything denied, only the egress proxy port, the interpreter and the run dir allowed

(version 1)(allow default)
(deny network*)
(allow network-outbound (remote ip "localhost:PROXY_PORT"))
(deny file-read* (subpath "/Users"))
(allow file-read* (subpath "INTERPRETER_DIR") (subpath "RUN_DIR"))
(deny file-write*)
(allow file-write* (subpath "RUN_DIR"))
(deny process-exec)
(allow process-exec (literal "INTERPRETER_DIR/bin/node"))

Check that the old blacklist profile leaks the Docker socket

sandbox-exec -p '(version 1)(allow default)(deny file-write*)(deny network-outbound (remote ip "localhost:*"))' curl -s --unix-socket /var/run/docker.sock http://localhost/_ping   # prints OK = leak

Context

Running user-supplied scripts (workflow custom code, plugins) in a child process on a macOS dev machine that also runs Docker and a local database.

Revisions (0)

No revisions yet.