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:
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:
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.
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
There are three ways to prevent malicious clients from sending unwanted HTTP traffic on the Paraxial.io free tier plan:
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:
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.