Packages
Multi-surface application runtime for Elixir. One TEA module renders to terminal, browser (LiveView), SSH, and MCP (agents). 30+ widgets, flexbox + CSS grid, AI agent runtime, distributed swarm with CRDTs, time-travel debugging, session recording, sandboxed REPL, and agentic commerce.
Current section
Files
Jump to
Current section
Files
lib/raxol/repl/sandbox.ex
defmodule Raxol.REPL.Sandbox do
@moduledoc """
AST-based safety checker for REPL code evaluation.
Scans Elixir code for potentially dangerous operations before evaluation.
Three strictness levels:
- `:none` -- allow everything (local terminal use)
- `:standard` -- deny destructive system/file/network ops (default)
- `:strict` -- whitelist-only (SSH/web use)
iex> Sandbox.check("Enum.map([1,2], & &1 * 2)")
:ok
iex> match?({:error, _}, Sandbox.check(~s[System.cmd("rm", ["-rf", "/"])]))
true
"""
@type level :: :none | :standard | :strict
@denied_standard [
{System, :cmd, "system command execution"},
{System, :shell, "shell command execution"},
{System, :halt, "system halt"},
{System, :stop, "system stop"},
{Port, :open, "port execution"},
{Port, :command, "port command"},
{File, :rm, "file deletion"},
{File, :rm!, "file deletion"},
{File, :rm_rf, "recursive file deletion"},
{File, :rm_rf!, "recursive file deletion"},
{File, :write, "file write"},
{File, :write!, "file write"},
{File, :rename, "file rename"},
{File, :rename!, "file rename"},
{File, :chmod, "file permission change"},
{File, :chmod!, "file permission change"},
{File, :chown, "file ownership change"},
{File, :chown!, "file ownership change"},
{Code, :eval_string, "dynamic code evaluation"},
{Code, :eval_file, "file code evaluation"},
{Code, :eval_quoted, "dynamic code evaluation"},
{Code, :compile_string, "dynamic code compilation"},
{Code, :compile_file, "dynamic code compilation"},
{:os, :cmd, "OS command execution"},
{:erlang, :halt, "VM halt"},
{:erlang, :open_port, "port execution"},
{:init, :stop, "VM stop"},
{Process, :exit, "process termination"},
{Node, :spawn, "remote code execution"},
{Node, :spawn_link, "remote code execution"},
{Node, :connect, "node connection"},
{GenServer, :call, "arbitrary GenServer interaction"},
{GenServer, :cast, "arbitrary GenServer interaction"},
{Kernel, :apply, "dynamic function application"}
]
@allowed_strict_modules [
Enum,
Stream,
Map,
Keyword,
List,
Tuple,
MapSet,
String,
Integer,
Float,
Atom,
IO,
Kernel,
Range,
Regex,
Date,
Time,
DateTime,
NaiveDateTime,
Calendar,
Access,
Base,
URI,
Jason,
Inspect
]
@denied_erlang_modules [:file, :net_adm, :gen_tcp, :gen_udp, :httpc, :ssl]
@doc """
Checks code for safety violations at the given strictness level.
Returns `:ok` if safe, or `{:error, [violation_message]}` if violations found.
"""
@spec check(String.t(), level()) :: :ok | {:error, [String.t()]}
def check(code, level \\ :standard)
def check(_code, :none), do: :ok
def check(code, level) do
case Code.string_to_quoted(code) do
{:ok, ast} ->
violations = scan(ast, level)
if violations == [], do: :ok, else: {:error, Enum.uniq(violations)}
{:error, {_meta, message, _token}} ->
{:error, ["Syntax error: #{message}"]}
end
end
defp scan(ast, level) do
{_ast, violations} =
Macro.prewalk(ast, [], fn node, acc ->
new_violations = check_node(node, level)
{node, new_violations ++ acc}
end)
Enum.reverse(violations)
end
defp check_node(
{{:., _, [{:__aliases__, _, mod_parts}, func]}, _, _args},
:standard
) do
module = Module.concat(mod_parts)
check_denied_call(module, func)
end
defp check_node({{:., _, [mod, func]}, _, _args}, :standard)
when is_atom(mod) do
if mod in @denied_erlang_modules do
["#{inspect(mod)}.#{func} is not allowed (dangerous erlang module)"]
else
check_denied_call(mod, func)
end
end
defp check_node(
{{:., _, [{:__aliases__, _, mod_parts}, func]}, _, _args},
:strict
) do
module = Module.concat(mod_parts)
if module in @allowed_strict_modules do
check_denied_call(module, func)
else
["#{inspect(module)}.#{func} is not allowed (module not in whitelist)"]
end
end
defp check_node({{:., _, [mod, func]}, _, _args}, :strict)
when is_atom(mod) do
if mod in @allowed_strict_modules do
check_denied_call(mod, func)
else
["#{inspect(mod)}.#{func} is not allowed (module not in whitelist)"]
end
end
defp check_node({:apply, _, args}, _level) when is_list(args) do
["apply is not allowed (dynamic function application)"]
end
defp check_node({:send, _, args}, _level) when is_list(args) do
["send is not allowed (message sending to arbitrary processes)"]
end
defp check_node({kind, _, _}, _level)
when kind in [:defmodule, :defprotocol, :defimpl] do
["#{kind} is not allowed (runtime module definition)"]
end
defp check_node(_node, _level), do: []
defp check_denied_call(module, func) do
case Enum.find(@denied_standard, fn {m, f, _} ->
m == module and f == func
end) do
{_, _, reason} ->
["#{inspect(module)}.#{func} is not allowed (#{reason})"]
nil ->
[]
end
end
end