Elixir and Phoenix Security Checklist: 11 Best Practices

Michael Lubas, 2025-02-11

Coding a web application in Elixir with the Phoenix framework gives you an incredibly solid base. Any security problems that occur are preventable, if you know what to watch out for. The list below is based on my own professional experience pentesting Elixir applications, and building a company to help developers with security. My goal is to help you avoid a data breach due to your project being hacked, each line item was selected with that consideration in mind.

The full list ranked in order of priority:

  1. Maintain an inventory of all your public facing applications
  2. Run static analysis scans with Sobelow regularly, in CI/CD if possible
  3. Detect vulnerable dependencies with MixAudit (NOT Dependabot), in CI/CD if possible
  4. Check the ports and SSH settings on your web server
  5. Ensure your database server is not exposed to the public internet
  6. Check for server side request forgery (SSRF), use the safeURL library if needed
  7. Be wary of using ImageMagick, Ghostscript, and FFmpeg on user uploads
  8. Rate limiting on authentication and payment endpoints
  9. Scan your code base for secrets
  10. Check for sequential IDs and access control violations
  11. Check for mass assignment in Ecto

Maintain an inventory of all your public facing applications

How many Elixir applications do you currently have deployed? How many are accessible by anyone on the internet? What version of Phoenix, Ecto, and Plug are they using? These are the basic questions an asset inventory can answer. When the news of a major vulnerability becomes public, the highest priority goal of a security team suddenly becomes:

  1. Identify all the software in the organization that is affected by the vulnerability. Internet facing software is the highest priority because attackers use automated scanners to identify and exploit vulnerable servers.
  2. Upgrade the software to the safe version.

When the Log4Shell vulnerability became public, every company using Java had to check the version of Log4j they were using and update each vulnerable system before an attacker could break in. That statement assumes a company has a list of each application using Log4j. When the vulnerability dropped, the number one priority of many security teams suddenly became hunting down legacy Java applications.

The good news is that if you’re an independent developer or small business a full inventory may be a straightforward task. Asset management is much more difficult for large organizations.


Run static analysis scans with Sobelow regularly, in CI/CD if possible

You may already be familiar with the OWASP Top 10, which includes vulnerabilities such as remote code execution (RCE), SQL injection, and cross site scripting (XSS). Sobelow is an open source SAST tool, which takes source code as input, and outputs a list of these vulnerabilities.

I’ve personally contributed to Sobelow, written a guide on how to fix each finding, and posted about how to get started using Sobelow in your own work. The biggest hurdle to getting started is running the initial scan and triaging all the findings as true/false positive. The article Elixir Security: Real World Sobelow goes into detail on how to do that.

Ideally you want to run Sobelow on each new code change in CI/CD to prevent vulnerabilities from ever being introduced into production.


Detect vulnerable dependencies with MixAudit (NOT Dependabot), in CI/CD if possible

I was personally surprised to learn that GitHub’s Dependabot does not support Elixir in dependency graph. This creates the unfortunate situation where it seems like your project is protected (Dependabot will open pull requests):

But it will not actually alert you to a vulnerable library:

If you are using GitLab instead of GitHub, there is no Elixir support.

What you want to use is MixAudit, an open source CLI tool that will alert you if your Elixir dependencies are vulnerable. This can also be run in CI/CD along with Sobelow.


Check the ports and SSH settings on your web server

This is not necessary if you are on a PaaS (Heroku, Gigalixir, Railway, Render, Fly.io), but is relevant if you are managing a web server on AWS, GCP, Hetzner, etc.

Generally speaking, the only ports you want to expose to the public internet are 80 and 443. Exposing port 22 to the public internet does happen, but you should avoid it if possible.

There are applications that may be running on your web server that should never be exposed publicly, including:

Docker Daemon (TCP 2375/2376) 
Redis (TCP 6379) 
Elasticsearch (TCP 9200/9300) 
Kibana (TCP 5601) 
Etcd (TCP 2379/2380)

All of these can lead to your entire web server being compromised by someone scanning for vulnerable hosts.

It’s also a good idea to enable public key authentication for SSH, so you eliminate the risk of accidentally setting a weak password. Bots are constantly searching the internet for servers with weak credentials, and using the public key setting eliminates this risk.

Further reading: Security Best Practices for Deploying Rails 8 on Linux with Kamal - The article is about Kamal and Rails, but the server layer advice is applicable to Elixir. For example: use the firewall service from your cloud provider to restrict network ports.

To run a full port scan of your web server, use nmap:

nmap -p- -T5 --min-rate=10000 --max-retries=1 <target>

Starting Nmap 7.94 ( https://nmap.org ) at 2025-02-06 13:56 EST
Nmap scan report for dev.paraxial.io (52.1.190.78)
Host is up (0.016s latency).
rDNS record for 52.1.190.78: ec2-52-1-190-78.compute-1.amazonaws.com
Not shown: 65532 filtered tcp ports (no-response)
PORT    STATE SERVICE
80/tcp  open  http
443/tcp open  https

Nmap done: 1 IP address (1 host up) scanned in 13.38 seconds

Ensure your database server is not exposed to the public internet

I considered merging this line and the previous one, but feel it’s important to emphasize that your database should never be exposed to the public internet. DeepSeek is currently in the news because it seems they ignored the secure defaults of ClickHouse and left it available on the public internet.

If you’re using Elixir/Phoenix your database is most likely Postgres, either running on the same web server, or in the same VPC, for example on RDS. It is critical that your database is not exposed to the public internet. This can happen:

  1. If your database is on the same web server, and the nmap scan above shows port 5432 (Postgres) or 3306 (MySQL) is open, your database is exposed.
  2. If the database is on a separate server, it should not be possible to connect to it from the public internet.

You may be asking: If the database is not exposed to the public internet, how do I access it? The answer is via SSH tunneling. An example setup:

  1. Restrict SSH access to your server, so only your home/office IP can connect. This reduces the risk of brute-force attacks, exploits, and unauthorized access.
  2. Have your database on a separate server, which can communicate with the web server, but is not exposed to the public internet.
  3. Configure your DB client (for example, Postico), to connect via SSH through the web server to your DB server.

This also allows you to log access to the database via SSH records.


Check for server side request forgery (SSRF), use the safeURL library if needed

Does your project allow users to enter URLs, which you then use to make HTTP requests? If so, you need to be aware of server side request forgery, or SSRF. Sobelow does not scan for this problem, so you will need to check for it yourself. This vulnerability was the root cause of the 2019 Capital One Hack. For a description of what SSRF is, see this Portswigger article.

Recently on a pentest I came across the following Elixir code using the Req HTTP library:

req = Req.new(base_url: "https://api.github.com")

Req.get!(req, url: "/repos/sneako/finch").body["description"]
#=> "Elixir HTTP client, focused on performance"

The codebase was setup so that external user input was being passed into the following:

Req.get!(req, url: user_input)

Where the %Req.Request struct had a base_url set:

iex(3)> req = Req.new(base_url: "https://elixir-lang.org/")
%Req.Request{
  method: :get,
  url: URI.parse(""),
  headers: %{},
  body: nil,
  options: %{base_url: "https://elixir-lang.org/"},

The application was not vulnerable to SSRF, however there was an interesting quirk, where if a user were able to submit a fully formed URL, it would override the base_url:

req = Req.new(base_url: "https://elixir-lang.org/")
user_input = "https://dashbit.co/blog"
Req.get!(req, url: user_input) 

The above code sends a request to https://dashbit.co/blog, NOT https://elixir-lang.org/. This is relevant for security because an attacker could change base_url from a static value to http://169.254.169.254, which is the AWS instance metadata endpoint.

A recent post from Alex Leahu @ Include Security describes a situation with Tesla which “resulted in server-side request forgery in a real engagement”.

plug Tesla.Middleware.BaseUrl, "https://example.com/foo"

MyClient.get("http://example.com/bar") # equals to GET http://example.com/bar

In summary:

  1. Do not send outbound network connections based on user input. In practice, this means check every place your project makes HTTP requests and ensure the URL is not set via user input.
  2. If this is a requirement, use the SafeURL library.

Be wary of using ImageMagick, Ghostscript, and FFmpeg on user uploads

Can users upload images, PDFs, or videos to your project? If you are simply taking the file and hosting it in AWS S3 or Cloudflare R2, that is a good practice which minimizes the risk of a security incident.

Are you running ImageMagick, Ghostscript, FFmpeg, or related file processing software on user uploads? That is a major security risk. This point could be an entire article, so to be brief, these libraries are extremely powerful and can lead to your entire web server being hacked if you are using an outdated version:

  1. ImageTragick, a widespread RCE in ImageMagick
  2. RCE bug in widely used Ghostscript library now exploited in attacks
  3. HackerOne FFmpeg SSRF in TikTok

If processing user file uploads is critical to your application, for example converting video files or resizing images, I highly recommend you pay for a professional security assessment by someone with experience in this area.


Rate limiting on authentication and payment endpoints

If your application has the following:

  1. Login with email/password
  2. Forgot password function
  3. New user signup (exposed to public internet)
  4. Payment with credit card

It is inevitable that someone will run a bot attack against your site. One IP address sending 100 requests per minute to any of the above endpoints is obviously malicious and should be banned. The impact of this attack can range from user accounts being compromised to your payment processor banning your account due to automated carding fraud.

There are many commercial offerings available to help with this problem. If you are looking for an open source DIY solution, see the article Throttling and Blocking Bad Requests in Phoenix Web Applications with PlugAttack.


Scan your code base for secrets

Having a secret value stored in source code may not be a security incident in and of itself. The reason it’s considered a best practice to keep secrets out of source code is shown by the following examples:

  1. Consider a source code repo stored in GitHub, set to private. If the repo is accidentally set to "public", or there is a bug in the GitHub access control code, or a user with access to the repo has their account compromised, then every secret has to be rotated.
  2. If you are using Docker Hub for deployments, and the registry is not set to private, all the secrets in source code will be exposed publicly.
  3. By keeping secrets in a dedicated environment, the risk of compromise is significantly reduced, because there is no "make public" button (which does exist for source code repos and Docker Registry).

To find secrets both in your source code and git history, use the open source tool Gitleaks.


Check for sequential IDs and access control violations

Consider two banking applications, where a user views their balance by visiting the following:

  /account/overview/8345
  /account/overview/13cfa889-8710-1d1b-acc8-93b5c1dbd62b 

The second option is the better one, for a few reasons:

  1. The bank may not want a new user to be able to see how many accounts total were in the system when they signed up.
  2. In the event of a bug in the authentication code, where users can view accounts that do not belong to them by changing the URL parameter, the impact is much lower when that parameter is a random UUID vs a sequential integer.

There was a high profile example where the email addresses of 100,000 iPad customers were exposed due to this type of vulnerability, often called insecure direct object reference or IDOR. Sobelow does not scan for this, hence the individual entry in this list.


Check for mass assignment in Ecto

The term mass assignment comes from Rails. The summary is that in older versions of Rails, all the attributes for a model could be updated via external user input by default. This is bad because imagine a User model with the attribute is_admin. You don’t want someone to submit the parameter is_admin=true when they don’t have permission to create an admin account. For a write up on a famous incident with GitHub, see How Homakov hacked GitHub. Rails 4 enabled protection against mass assignment by default, so it’s not seen that often today.

The design of Ecto in Phoenix takes the risk of mass assignment into consideration, because you have to explicitly define what parameters are allowed to be set from user supplied data.

Consider a simple users schema:

  schema "users" do
    field :email, :string
    field :password, :string, virtual: true, redact: true
    field :hashed_password, :string, redact: true
    field :confirmed_at, :naive_datetime
    field :is_admin, :boolean 

    timestamps()
  end


  def registration_changeset(user, attrs, opts \\ []) do
    user
    |> cast(attrs, [:email, :password, :is_admin])
    |> validate_email()
    |> validate_password(opts)
  end

Assume that the corresponding signup form is exposed to the public internet. Can you spot the vulnerability?

The problem is that :is_admin should never be set via external user input. Anyone on the public internet can now create a user where “is_admin” is set to true in the database, which is likely not the intent of the developer.

This is another access control problem that Sobelow cannot scan for, so be careful if there are authentication and authorization related fields in your changeset. Personally I have not seen mass assignment on Elixir pentest engagements, but the risk is still there.


Paraxial.io stops data breaches by helping developers ship secure applications. Get a demo or start for free.

Subscribe to stay up to date on new posts.