Raxol. REPL. Sandbox
(Raxol v2.8.0)
View Source
AST-based safety checker for REPL code evaluation.
Not a security boundary
This is a MITIGATION. It raises the cost of the obvious attempts; it does not
confine anything. A gap in it reaches the OS user the BEAM node runs as, with
that node's full authority: its file handles, its network, its signing keys if
it has any. Read the trust-boundary section of Raxol.REPL.Evaluator before
putting this in front of input you do not trust.
Why it cannot be a boundary: every clause decides safety from a module NAME,
resolved statically from the submitted AST. Anything that changes which module
a name reaches at runtime sits underneath the check rather than inside it,
which is why import, alias, require and use are refused outright
rather than resolved (#1045). Each such form found so far has been a
CVE-class hole on an anonymous surface, and the checker's own history is the
argument against trusting it: computed receivers, Module.concat/1
alias resolution, the bare-name capture forms of apply and spawn,
maps forged with a __struct__ key, sorters naming a runtime module
(Enum.sort(l, m) calls m.compare/2), and raise, calendar, time
zone database and Map.from_struct/1 arguments holding one
(DateTime.now(tz, m) calls m.time_zone_period_from_utc_iso_days/2)
were each :ok at :strict until they were not.
A forged struct is the same hole reached through data rather than a name. A
map whose __struct__ key names a module is protocol-dispatched
(Collectable, Enumerable, String.Chars, Inspect, Access) to that
module's implementations, so collecting into
%{__struct__: File.Stream, ...} writes a file without File ever
appearing in a call. :strict refuses every form it knows can set that key
where a map is built: a :__struct__ or computed map key, a struct literal
outside a short allowlist, and the calls that write keys or values from
runtime data (Map.put/3 with a computed key, Map.new/1 or Enum.into/2
into a map from a computed input, put_in/3 with a computed path, ...). It
also refuses every __-prefixed function or macro of an allowlisted module,
called or captured: the generated URI.__struct__/1 writes a caller's
:__struct__ pair as readily as a map literal does.
That closes what the source shows, not what runtime data can hide: a form
this list does not name reaches the same implementations, and only the
OS-isolated peer node of #1231 closes the class.
What actually keeps an untrusted caller away from the evaluator is the
deployment flag, Raxol.Core.Boundary.Evaluation.exposed?/0. A node with that
flag set is a node you have decided may run submitted code.
Levels
:none-- allow everything (explicit trusted-local launcher opt-in):standard-- deny destructive and process-creation operations (check/1default):strict-- whitelist-only (the level every served launch gets)iex> Sandbox.check("Enum.map([1,2], & &1 * 2)") :ok
iex> match?({:error, _}, Sandbox.check(~s[System.cmd("rm", ["-rf", "/"])])) true
Summary
Functions
Checks code for safety violations at the given strictness level.
Types
Functions
Checks code for safety violations at the given strictness level.
Returns :ok if safe, or {:error, [violation_message]} if violations found.
Raises ArgumentError for a level other than :none, :standard or
:strict.