Secure Your Gigalixir App with Paraxial.io

Michael Lubas, 2024-08-27

Gigalixir is a popular Platform as a Service (PaaS) provider for Elixir applications, for good reason. The service was built from the ground up for Elixir developers, from individuals deploying their first application to businesses relying on the service for critical revenue generating applications. When it comes to securing an Elixir application, support from legacy security vendors is bad, and often requires mucking about with docker, installing an additional language (such as Java), and fails to secure applications hosted on a PaaS such as Gigalixir.

This article will walk through using Paraxial.io to secure an Elixir application running on Gigalixir. If you would like to follow along with your own app, both Gigalixir and Paraxial.io have free tiers, you can sign up here:

Gigalixir Registration

Paraxial.io Registration

Introduction

The open source application Potion Shop will be deployed on Gigalixir for this example. However, there is a problem, the application is vulnerable to several security problems: remote code execution (RCE), SQL injection, cross site scripting (XSS), and more. Any code that is deployed on the public internet can be attacked by malicious clients continuously scanning for vulnerable endpoints. If you deploy Potion Shop in its current vulnerable state, someone could break into your server.

What is the impact if your application gets hacked? If you’re just hosting a personal project, the risk seems tiny, but several possible scenarios include:

  1. If you're collecting email and password pairs, those can be stolen and used by the bad guy to commit further crimes.
  2. Even if you don't handle any sensitive data, if your sever is hacked, it can be used to mine bitcoin, send spam, or serve fraudulent traffic for a botnet operator.

If the Elixir application you are using belongs to a business handling customer data, security is even more important. Data breaches lead to a collapse in customer trust, fines, and legal penalties. The good news is Elixir provides you with an excellent base to build upon, and Paraxial.io can automate this process for you.

Back to Potion Shop, it has several preventable security problems. You will need a local Elixir installation to continue.

% git clone https://github.com/securityelixir/potion_shop.git
% cd potion_shop 
% mix deps.get
% mix ecto.setup
% mix phx.server

Your local setup should look similar to the below example before continuing:

Now create a Paraxial.io account and run your first scan. The documentation page goes into more detail, the summary is you should create a Paraxial.io account, create a site (since your local environment does not have a real domain name, you can make up a memorable comment, for example michael-potion-shop), and get your site’s API key (found in settings).

To install Paraxial.io, remove the sobelow and mix_audit dependencies, and add paraxial:

mix.exs

  defp deps do
    [
      {:bcrypt_elixir, "~> 3.0"},
      ...
      {:paginator, "~> 0.6.0"},
      {:sobelow, "~> 0.13", only: [:dev, :test], runtime: false}, # Remove
      {:mix_audit, "~> 2.1"}, # Remove
      {:paraxial, "~> 2.7.7"}
    ]
  end

Save the new mix.exs file and run:

% mix deps.get

Then add your site’s API key as an environment variable:

export PARAXIAL_API_KEY=your_value_here

And run:

% mix paraxial.scan

Expected output:

The scan shows a total of 9 vulnerabilities:

HTTPS will be set via the server configuration, and we would like to defer setting up a content security policy for now. To skip these findings, create the following file in the Potion Shop root directory:

cat .sobelow-conf
[private: true, skip: true, format: "json", ignore: ["Config.CSP", "Config.HTTPS"], ignore_files: []]

% mix paraxial.scan --sobelow-config

Expected output:

Paraxial.io provides a guide for how to fix each identified vulnerability:

To prepare Potion Shop for deployment on Gigalixir, here is a summary of how to fix each vulnerability:

SQL Injection

SQL injection allows an attacker to inject unauthorized SQL commands, leading to a data breach. Further reading: Detecting SQL Injection in Phoenix with Sobelow

View secret Potions: http://localhost:4000/?name=%27+OR+1%3D1%3B–

lib/carafe/potions.ex

# Unsafe 
  def search_potions(name) do
    q = """
    SELECT p.id, p.name, p.milliliters, p.price, p.secret
    FROM potions as p
    WHERE p.name LIKE '%#{name}%' AND p.secret = false
    """
    {:ok, %{rows: rows}} =
      Ecto.Adapters.SQL.query(Repo, q)
    Enum.map(rows, fn row ->
      [id, name, milliliters, price, secret] = row
      %Potion{id: id, name: name, milliliters: milliliters, price: price, secret: secret}
    end)
  end


# Safe
  def search_potions(name) do
    query = from p in Potion,
      where: like(p.name, ^"%#{name}%") and p.secret == false

    Repo.all(query)
  end

For the SQL injection vulnerability, using the security features built into Ecto queries fixes the vulnerability.

Remote Code Execution in Paginator

Remote Code Execution is the worst case scenario for web application, and an attacker who exploits the vulnerability gets production SSH access. Further reading: Elixir/Phoenix Security: Remote Code Execution and Serialisation

To fix the vulnerability, simply update the paginator library:

mix.exs

{:paginator, "~> 1.2.0"}

% mix deps.get

Upgraded:
  db_connection 2.6.0 => 2.7.0
  ecto 3.11.2 => 3.12.1
  ecto_sql 3.11.1 => 3.12.0
  jason 1.4.1 => 1.4.4
  paginator 0.6.0 => 1.2.0 (major)
  postgrex 0.17.5 => 0.19.1 (minor)

Cross Site Scripting (XSS)

Cross site scripting (XSS) allows an attacker to inject JavaScript into a victim’s browsing session. A high profile and interesting example of this attack was the MySpace Samy worm.

Further reading: Cross Site Scripting (XSS) Patterns in Phoenix

In Potion Shop, the call to raw is unnecessary, and should be removed:

lib/carafe_web/templates/potion/show.html.heex

# Unsafe 
<div><%= raw review.body %></div>

# Safe
<div><%= review.body %></div>

Phoenix escapes malicious user input by default, so XSS is rare in Elixir apps, although you can bypass this protection with raw, leading to the vulnerability seen here.

Cross Site Request Forgery (CSRF)

There are two CSRF vulnerabilities in Potion Shop. One is due to a missing plug :protect_from_forgery in a router pipeline, and the other is known as Action Reuse CSRF in Phoenix, where a GET request can trigger the same controller action as a POST request, bypassing the CSRF protection. Further reading:

Introduction to Cross Site Request Forgery (CSRF)

What is CSRF via Action Reuse?

lib/carafe_web/router.ex

# Unsafe
  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

# Safe
  pipeline :browser_auth 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
# Unsafe 
  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


# Safe

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

    # Delete the `get` line
    post "/users/settings/edit_bio", UserSettingsController, :edit_bio

Now when you run:

% mix paraxial.scan --sobelow-config

The expected result is 0 findings. Now that you have fixed the outstanding issues, it is time to deploy Potion Shop on Gigalixir.

Run Potion Shop on Gigalixir

Ensure your are logged into Gigalixir in your terminal:

% gigalixir login

Go to the Potion Shop directory and run:

potion_shop % echo "elixir_version=1.15.7" > elixir_buildpack.config
potion_shop % echo "erlang_version=26.1" > elixir_buildpack.config
potion_shop % gigalixir create
Created app: darkgreen-big-brant.
Set git remote: gigalixir.
darkgreen-big-brant

potion_shop % gigalixir pg:create --free
potion_shop % git push gigalixir
-----> Slug found.
remote: Uploading slug.
remote: Creating release.
remote: Starting zero-downtime rolling deploy.
remote: Please wait a minute for the new instance(s) to roll out and pass health checks.
remote: For troubleshooting, See:      http://gigalixir.readthedocs.io/en/latest/main.html#troubleshooting
remote: For help, contact:             help@gigalixir.com
remote: Try hitting your app with:     curl YOUR_URL_HERE

The URL for your application will be unique to your app. Visiting it will show an error:

This is because you have to run the Ecto database migration to continue. The Gigalixir documentation mentions adding your id_rsa.pub key. On my machine this was id_ed25519.pub, the summary is you need a public ssh key. Add it then run the migration:

Docs say:
% gigalixir account:ssh_keys:add "$(cat ~/.ssh/id_rsa.pub)"

My version: 
% gigalixir account:ssh_keys:add "$(cat ~/.ssh/id_ed25519.pub)"

% gigalixir ps:migrate

The error is gone, but the potions are not loaded. To display the potions you need to run the seeds.exs file in production. Since you added an ssh key in the last step, this is simple:

% gigalixir ps:remote_console
> seed_script = Path.join(["#{:code.priv_dir(:carafe)}", "repo", "seeds.exs"])
"/app/lib/carafe-0.1.0/priv/repo/seeds.exs"

> Code.eval_file(seed_script)

Now that Potion Shop is deployed, configure Paraxial.io to run in production:

config/prod.exs

config :paraxial,
  paraxial_api_key: System.get_env("PARAXIAL_API_KEY"),
  fetch_cloud_ips: true,
  exploit_guard: :block
% gigalixir config:set PARAXIAL_API_KEY=your_value_here

% git add .
% git commit -m "Add Paraxial.io to production config"
% git push

% git push gigalixir 

Stop Bot Attacks with Paraxial.io

There are three ways to prevent malicious clients from sending unwanted HTTP traffic on the Paraxial.io free tier plan:

  1. Rate limiting on IP address
  2. Blocking known bot IPs via the plug Paraxial.BlockCloudIP
  3. Banning clients who submit a honeypot HTML form (only visible to bots)

Start by explicitly setting a development config for Paraxial.io:

config/dev.exs

config :paraxial,
  paraxial_api_key: System.get_env("PARAXIAL_API_KEY"),
  fetch_cloud_ips: false,
  exploit_guard: :block

fetch_cloud_ips results in an HTTP request from your local project to the Paraxial backend to get the radix trie of IP addresses. We will be using this feature in prod, but for dev you can disable it. The goal is to rate limit login events:

lib/carafe_web/router.ex

  ## Authentication routes

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

    ...
    post "/users/log_in", UserSessionController, :create   # This one

Add the following code

lib/carafe_web/controllers/user_session_controller.ex

  def create(conn, %{"user" => user_params}) do
    ip_string = conn.remote_ip |> :inet.ntoa() |> to_string()
    key = "user-login-post-#{ip_string}"
    seconds = 30
    count = 3
    ban_length = "hour"
    ip = conn.remote_ip
    msg = "> 3 POSTs in 30 seconds to #{conn.host}/users/log_in from #{ip_string}"

    %{"email" => email, "password" => password} = user_params

    case Paraxial.check_rate(key, seconds, count, ban_length, ip, msg) do
      {:allow, _} ->
        if user = Accounts.get_user_by_email_and_password(email, password) do
          UserAuth.log_in_user(conn, user, user_params)
        else
          # In order to prevent user enumeration attacks, don't disclose whether the email is registered.
          render(conn, "new.html", error_message: "Invalid email or password")
        end
      {:deny, _} ->
        conn
        |> put_resp_content_type("text/html")
        |> send_resp(401, "Banned")
    end
  end

http://localhost:4000/users/log_in

Now make some requests and see if you get banned:

Before deployment, we have to add remote_ip for the value conn.remote_ip to be set correctly.

mix.exs

{:remote_ip, "~> 1.2"}

lib/carafe_web/endpoint.ex

  plug Plug.Session, @session_options
  plug RemoteIp # Must be the line before the router
  plug Paraxial.AllowedPlug                  # Determines if an HTTP request should be allowed or blocked before it reaches the router
  plug CarafeWeb.Router
% mix deps.get

% git add .
% git commit -m "rate limit and add paraxial plugs"
% git push gigalixir

Now when you perform the attack, the IP will be banned:

The Paraxial.io Slack App will notify you when an IP is banned, and is available on the free tier: https://hexdocs.pm/paraxial/slack_app.html

Additional features included in the free tier:

Blocking known bot IPs via the plug Paraxial.BlockCloudIP. To learn more about how this attack works, see How attackers bypass IP based rate limiting.

Banning clients who submit a honeypot HTML form (only visible to bots).

Exploit Guard - Blog Post and Paraxial.io Docs

GitHub App - Blog Post and Paraxial.io Docs

The above documentation can be used to complete the additional setup. A SaaS product built on Gigalixir should be using the following Paraxial.io features:

  1. The GitHub App, to check all new code changes for security problems. If you prefer not to use the app, `mix paraxial.scan` runs the same scan. This checks for code layer vulnerabilities with Sobelow, outdated dependencies, and retired dependencies.
  2. Exploit Guard, to block remote code execution (RCE) attacks at runtime.
  3. Bot Defense should be used on all authentication routes (login, password reset, account creation) and routes related to credit card transactions. The following are available on the free tier:
    1. IP rate limiting
    2. Blocking IP addresses belonging to rented cloud servers
    3. Banning IP addresses that submit a fake HTML form
  4. The Paraxial.io Slack App, to receive notifications about bot attacks in real time.

Gigalixir is an excellent place to host your Elixir app, and with Paraxial.io you can ensure your deployment is safe and secure.


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.