Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

ESCT: Part 5 - Elixir Security

modules/5-elixir.livemd

ESCT: Part 5 - Elixir Security

Section

Mix.install([
  {:grading_client, path: "#{__DIR__}/grading_client"},
  :benchwarmer,
  :kino,
  :plug
])

Introduction

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.

Table of Contents

Atom Exhaustion

Description

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.

Prevention

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.

Resources

  1. https://erlef.github.io/security-wg/secure_coding_and_deployment_hardening/atom_exhaustion

Quiz

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)

Serialization and Deserialization

Description

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

Prevention

  • 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

Resource

  1. https://erlef.github.io/security-wg/secure_coding_and_deployment_hardening/serialisation

Untrusted Code

Description

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.

Prevention

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.

Resources

  1. https://erlef.github.io/security-wg/secure_coding_and_deployment_hardening/sandboxing

Example

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 😉

Timing Attacks

Description

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.

Prevention

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.

Resources

  1. https://erlef.github.io/security-wg/secure_coding_and_deployment_hardening/timing_attacks

Quiz

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)

Boolean Coercion

Description

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.

Prevention

By using expressions that do not use boolean coercion, the incorrect assumption about the function’s return type is caught early:

  • Prefer case over if, unless or cond
  • 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.

Resources

  1. https://erlef.github.io/security-wg/secure_coding_and_deployment_hardening/elixir_truthy

Quiz

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 Exposure

Description

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!

Prevention

Wrapping

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.

Example

wrapped_secret = fn -> System.get_env("SECRET") end

Stacktrace Pruning

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

Example

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

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.

Quiz

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]

Resource

  1. https://erlef.github.io/security-wg/secure_coding_and_deployment_hardening/sensitive_data.html

<- Previous Module: GraphQL Security || Next Module: Cookie Security ->