Mix.install([
{:grading_client, path: "#{__DIR__}/grading_client"},
:benchwarmer,
:kino,
:plug
])
Luckily for developers in the Elixir ecosystem (and unluckily for folks trying to create Secure Coding Training materials), Elixir is a rather secure language by default.
But even the dullest blades can hurt someone! This module goes over Elixir specific insecurities to look out for when using the language to build with.
- Atom Exhaustion
- Serialization and Deserialization
- Untrusted Code
- Timing Attacks
- Boolean Coercion
- Sensitive Data Exposure
Each unique atom value in use in the virtual machine takes up an entry in the global atom table. New atom values are appended to this table as needed, but entries are never removed. The size of the table is determined at startup, based on the +t
emulator flag, with a default of 1,048,576 entries. If an attempt is made to add a new value while the table is at capacity, the virtual machine crashes.
Because of the above, care should be taken to not create an unbounded number of atoms. In particular, creating atoms from untrusted input can lead to denial-of-service (DoS) vulnerabilities.
The best way to prevent atom exhaustion is by ensuring no new atom values are created at runtime: as long as any atom value required by the application is referenced in code, that value will be defined in the atom table when the code is loaded.
The conversion of other types into atoms can then be constrained to only allow existing values using the to_existing_atom/1
function variants.
Beware of functions in applications/libraries that create atoms from input values.
Fix the vulnerable function below by changing the String function used on line 7.
You should get a true
result when you successfully fix the function.
random_number = :rand.uniform(10000)
malicious_user_input = Integer.to_string(random_number)
prev_count = :erlang.system_info(:atom_count)
try do
malicious_user_input
# ONLY CHANGE LINE 8
|> String.to_atom()
rescue
e -> {ArgumentError, e}
end
IO.puts("Are you protected against Atom Exhaustion?")
IO.puts(:erlang.system_info(:atom_count) == prev_count)
Deserialization of untrusted input can result in atom creation, which can lead to your application being vulnerable to denial of service (DOS) attacks. When you do use a deserialization library, make sure that the library does not create arbitrary atoms: either configure the library to return strings/binaries or enable schema validation to constrain the input
- Use the :safe option when calling
:erlang.binary_to_term/2
on untrusted input (should be familiar from atom exhaustion 😀) - Prevent function deserialisation from untrusted input, e.g. using
Plug.Crypto.non_executable_binary_to_term/1,2
The BEAM runtime has very little support for access control between running processes: code that runs somewhere in a BEAM instance has almost unlimited access to the VM and the interface to the host on which it runs. Moreover, a process on a node in a distributed Erlang cluster has the same level of access to the other nodes as well.
It is therefore not possible to isolate untrusted processes in some sort of sandbox.
Do not use Code.eval_file/1,2
, Code.eval_string/1,2,3
and Code.eval_quoted/1,2,3
on untrusted input, or in production code at all.
Below is a very contrived example of taking user input and running it as code. If you run the first code block, you can input your name (or anything you wish your own computer to run) in the following code block.
name = Kino.Input.text("What's your name?")
textfield_value = Kino.Input.read(name)
{result, binding} = Code.eval_string("a", a: textfield_value)
"Hello, " <> result
BONUS QUESTION: How would you go about securing the code above?
Hint: Deleting it entirely is a fair approach 😉
A timing attack is a side-channel attack in which the attacker attempts to compromise a cryptosystem by analyzing the time taken to execute cryptographic algorithms.
Plainly speaking, response time it takes to compute a given function measured at the pico-second level is analyzed for microscopic variations.
This technique is primarily used to analyze string comparisons of secret values to brute-force the identify of the secret.
e.g. When comparing two strings, the function exits when variation is detected. Take a secret value MY_SECRET
and a user input MY_PASSWORD
, the string compariosn (MY_PASSWORD == MY_SECRET
) would go character by character until there's a complete match or a discrepancy. So if the new input was MY_SAUCE
, that new string would take marginally longer to compare against the secret than MY_PASSWORD
because of one more similar character as MY_SECRET
.
It's simple enough to protect against these types of attacks, especially during string comparisons. You need to execute functions in a way the performs them in constant time. In Elixir (and many other languages), you can use a secure string compare function that will take the same time to compare strings since it won't immediately fail at the point of first difference and instead check the full length of the string every time.
Note: constant time operations tend to take longer, that is the trade off for security. But the difference is measured in clock cycles at that point.
Observe the two functions outlined below, one is susceptible to timing attacks and the other uses constant time to compare strings. Change the value of user_input
and see if you notice any time difference in the execution time.
Simply uncomment the IO.puts on the last line of the code block for credit on this question.
defmodule Susceptible do
def compare(input, value) do
case input do
^value -> :ok
_ -> :access_denied
end
end
end
defmodule Constant do
def compare(input, value) do
case Plug.Crypto.secure_compare(input, value) do
true -> :ok
false -> :access_denied
end
end
end
password = "HASH_OF_THE_USERS_ACTUAL_PASSWORD"
# DO NOT EDIT ANY CODE ABOVE THIS LINE =====================
user_input = "HASH_OF_asdfasdf"
# DO NOT EDIT ANY CODE BELOW THIS LINE (you may uncomment IO.puts) =============
Benchwarmer.benchmark(fn -> Susceptible.compare(user_input, password) end)
Benchwarmer.benchmark(fn -> Constant.compare(user_input, password) end)
# IO.puts(:comparison_ran)
Elixir has a concept of a "truthy" value, where anything other than false
or nil
is considered true
.
This can lead to subtle and unexpected bugs, especially when interworking with Erlang libraries. Imagine a library that performs cryptographic signature validation, with a return type of :ok | {:error, atom()}
. If this function were mistakenly called in a context where a "truthy" value is expected, the return value would always be considered true.
By using expressions that do not use boolean coercion, the incorrect assumption about the function's return type is caught early:
- Prefer
case
overif
,unless
orcond
- Prefer
and
over&&
- Prefer
or
over||
- Prefer
not
over!
The latter will raise a "BadBooleanError" when the function returns :ok or {:error, _}. In the interest of clarity if may even be better to use a case construct, matching explicitly on true and false.
The function SecurityCheck
below does not return a truthy value but is treated as such in both of the commented out function calls, which if statement is the correct way to call this function?
Uncomment the if statement that uses the correct boolean comparison.
defmodule SecurityCheck do
def validate(input, password_hash) do
case Plug.Crypto.secure_compare(input, password_hash) do
true -> :ok
false -> :access_denied
end
end
defexception message: "There was an issue"
end
password = "some_secure_password_hash"
user_input = "some_string_which_obviously_isnt_the_same_as_the_password"
:ok
# DO NOT EDIT ANY CODE ABOVE THIS LINE =====================
# if SecurityCheck.validate(user_input, password) or raise(SecurityCheck) do :you_let_a_baddie_in end
# if SecurityCheck.validate(user_input, password) || raise(SecurityCheck) do :you_let_a_baddie_in end
Sensitive data is any information that should be out of reach from all outsiders unless they have permission to access it, which in most cases would be considered "confidential data". Some examples of sensitive data are PHI (Protected Health Information) or PII (Personally Identifiable Information).
While it's obvious that we don't want data of this nature to get exposed, let's walk through some of the common instances in which it can occur while using Elixir and how to prevent it from happening!
Exceptions may result in console or log output that includes a stack trace. Mostly a stack trace shows the module/function/arity and the filename/line where the exception occurred, but for the function at the top of the stack the actual list of arguments may be included instead of the function arity.
To prevent sensitive data from leaking in a stack trace, the value may be wrapped in a closure: a zero-arity anonymous function. The inner value can be easily unwrapped where it is needed by invoking the function. If an error occurs and function arguments are written to the console or a log, it is shown as #Fun<...>
or #Function<...>
.
Secrets wrapped in a closure are also safe from introspection using Observer and from being written to crash dumps.
wrapped_secret = fn -> System.get_env("SECRET") end
Another approach, useful in functions that call the standard library (e.g. crypto) or other functions that do not support wrapping secrets in a closure, is stripping argument values from the stack trace when an exception occurs. This can be done by wrapping the function call(s) in a try … catch expression (Erlang) or adding a rescue clause to a function body (Elixir), and stripping the function arguments before re-raising the exception
def encrypt_with_secret(message, wrapped_secret) do
ComeCryptoLib.encrypt(message, wrapped_secret.())
rescue
e -> reraise e, prune_stacktrace(System.stacktrace())
end
defp prune_stacktrace([{mod, fun, [_ | _] = args, info} | rest]),
do: [{mod, fun, length(args), info} | rest]
defp prune_stacktrace(stacktrace), do: stacktrace
ETS tables are commonly used as a go to caching mechanism in-app. But did you know that they can be declared as private (through the use of the :private
option when instantiating them)?
This prevents the table from being read by other processes, such as remote shell sessions. Private tables are also not visible in Observer.
We have decided that we do not want this ETS table to be read from other processes, so try making it private:
# ONLY EDIT THIS LINE
secret_table = :ets.new(:secret_table, [:public])
:ets.info(secret_table)[:protection]
<- Previous Module: GraphQL Security || Next Module: Cookie Security ->