A sandbox for Elixir to safely evaluate untrusted code from user input.
Try it out on our online playground!
Dune
can be useful to develop playgrounds, online REPL, coding games, or
customizable business logic.
Warning: Dune
cannot offer strong security guarantees (see the
Security guarantees section below). Besides, it is still
early stage: expect bugs and vulnerabilities.
- allowlist mechanism (customizable) to restrict execution to safe modules and functions: no access to environment variables, file system, network...
- code executed in an isolated process
- execution within configurable limits: timeout, maximum reductions and memory (inspired by Luerl)
- captured standard output
- atoms, without atom leaks: parsing and runtime do not leak atoms (i.e. does not keep filling the atom table until the VM crashes)
- modules, without actual module creation: Dune does not let users define any
actual module (would leak memory and modify the state of the VM globally), but
defmodule
simulates the basic behavior of a module, including private and recursive functions
iex> Dune.eval_string("IO.puts(\"Hello world!\")")
%Dune.Success{inspected: ":ok", stdio: "Hello world!\n", value: :ok}
iex> Dune.eval_string("File.cwd!()")
%Dune.Failure{message: "** (DuneRestrictedError) function File.cwd!/0 is restricted", type: :restricted}
iex> Dune.eval_string("List.duplicate(:spam, 100_000)")
%Dune.Failure{message: "Execution stopped - memory limit exceeded", stdio: "", type: :memory}
iex> Dune.eval_string("Enum.product(1..100_000)")
%Dune.Failure{message: "Execution stopped - reductions limit exceeded", stdio: "", type: :reductions}
The list of modules and functions authorized by default is defined by the
Dune.Allowlist.Default
module, but this list can be extended and customized (at your own risk!) using
Dune.Allowlist
.
If you need to keep the state between evaluations, you might consider
Dune.Session
:
iex> Dune.Session.new()
...> |> Dune.Session.eval_string("x = 1")
...> |> Dune.Session.eval_string("x + 2")
#Dune.Session<last_result: %Dune.Success{inspected: "3", stdio: "", value: 3}, ...>
Dune.string_to_quoted/2
returns the AST corresponding to the provided
string
, without leaking atoms:
iex> Dune.string_to_quoted("foo(:bar)").inspected
"{:foo, [line: 1], [:bar]}"
Dune
supports a fair subset of the base language, but it cannot safely support
advanced features (at least at this stage) such as:
- custom structs / behaviours / protocols
- concurrency / processes / OTP
- metaprogramming
Because of the approch being used, Dune cannot offer strong security guarantees, and should not be considered a sufficient security layer by itself.
A best-effort approach is made to prevent attackers from getting outside of the original process and from calling any function/macro outside of the allowlist. However, it is impossible to prove that all escape paths have been completely blocked. Due to how the Erlang VM works, an attacker able to escape the sandbox could get full access to the VM without restriction.
See the EEF guidelines about sandboxing for more information.
Use at your own risk and avoid running it directly on a server with any sensitive access, e.g. to a database.
The package can be installed by adding dune
to your list of dependencies in
mix.exs
:
def deps do
[
{:dune, "~> 0.3.10"}
]
end
Documentation can be found at https://hexdocs.pm/dune.
Atoms are converted by the parser and mapped to always use a given set of atoms.
So when you type foo = bar(:baz)
, the atoms :foo
, :bar
and :baz
won't
actually be created, but other atoms like :atom1
, :atom2
and :atom3
are
going to be used instead.
So even if many user run various codes using many different variable and function names, they will all pull from the same pool of atoms.
Modules are being defined globally within the VM, making it both an isolation concern and a memory concern. But modules are an important part of the Elixir language, and are especially important for learning platforms.
Dune implements an alternative defmodule
, relying on maps of anonymous
functions at runtime and should reproduce the basic behavior of actual modules,
with support for recursive and private functions.
As explained above, some parts of the language are actually being completely reimplemented because the original version could not be safely sandboxed: atoms, modules... While these alternative implementation aim to be as close as possible to the original ones, they might differ in some cases, because the code being executed is actually different.
Some parts of the language are being restricted because they present a direct security risk, while some other would need to be reimplemented in an alternative way and therefore need a consequent amount of work that hasn't been done yet.