Common Web Application Vulnerabilities
- Cross Site Scripting (XSS)
- Cross Site Request Forgery (CSRF)
- Cross-Site WebSocket Hijacking (CSWSH)
- SQL Injection
- Denial of Service (DoS)
- Client-side enforcement of server-side security
In addition to being a valuable tool for the Elixir ecosystem as a whole, the static analysis tool Sobelow is highly recommended to detect common vulnerabilities particularly in Phoenix projects. Griffin Byatt, the creator of Sobelow, gave an ElixirConf talk on this subject when the tool debuted - speaking more on how it works under the hood.
While Sobelow to this day remains an excellent tool for Elixir developers to leverage while developing locally; it is recommended that Sobelow be additionally implemented in the CI/CD pipeline scanning code as it gets merged to the main branch. While we won’t go into detail how to set that process up here, it can be accomplished in a number of ways; whether through GitHub Actions or Docker images executing on CI runners.
The put_secure_browser_headers
Plug,
included in the “browser” pipeline in Phoenix routers by default, provides some
basic protections against a number of web application vulnerabilities. The exact
list of headers changes from one version of Phoenix to another, so please refer
to the latest documentation for details on which headers are being set and what
they protect against.
Cross Site Scripting (XSS)
Phoenix provides strong guard rails to prevent XSS by default. When rendering user input as HTML, be aware of the following:
- The
Phoenix.HTML.raw/1
function. If user input is being passed to this function, the application is vulnerable to XSS. - The
Phoenix.Controller.html/2
function. For example, in a Phoenix controller the following is vulnerable:
def html_resp(conn, %{"i" => i}) do
html(conn, "<html><head>#{i}</head></html>")
end
- Similar to the above example, constructed via a pipeline:
def send_resp_html(conn, %{"i" => i}) do
conn
|> put_resp_content_type("text/html")
|> send_resp(200, "#{i}")
end
- File upload functionality can lead to XSS if the content-type of the server response is being set by the user. Consider a Phoenix application that renders an uploaded file with the following:
def view_photo(conn, %{"filename" => filename}) do
case ImgServer.get(filename) do
%{content_type: content_type, bin: bin} ->
conn
|> put_resp_content_type(content_type)
|> send_resp(200, bin)
_ ->
conn
|> put_resp_content_type("text/html")
|> send_resp(404, "Not Found")
end
end
A malicious user could set the content_type to text/html
, and upload an HTML
document that executes JavaScript. When this file is viewed by a victim, the
XSS payload executes in the victim web browser.
Further Reading:
Content Security Policy
Content Security Policy (CSP) is a security feature that can be implemented on web applications to prevent certain types of attacks, such as cross-site scripting (XSS) and data injection attacks. CSP helps protect web applications by defining a whitelist of trusted sources for the content that the application can load or execute.
If someone is unfamiliar with web security and is using the Phoenix Elixir web framework, it is essential to understand the potential security risks that their application may face. With CSP, the developer can define a set of policies that restrict the types of resources that the application can load or execute. These policies include allowing only specific sources of images, scripts, stylesheets, fonts, and plugins.
Using Content Security Policy in the Phoenix Elixir web framework is a crucial security feature that helps protect web applications from various types of attacks. By restricting the sources of content that the application can load or execute, developers can reduce the risk of security vulnerabilities and ensure the safety of their users.
To learn more about Content Security Policy, see: https://content-security-policy.com/
To help create policies, see: https://report-uri.com/home/generate
Setup without external Dependencies
For a very simple setup, no external dependencies are required. If you need a more complicated setup including nonces etc, please refer to the next section.
Supply the following option to the
put_secure_browser_headers
Plug:
plug :put_secure_browser_headers, %{
"content-security-policy": "[Your Policy]"
}
Setup using plug_content_security_policy
plug_content_security_policy is a small library to aid configuring content security policies correctly. On top of just generating the header, it also helps with generating nonces.
To use plug_content_security_policy, install the dependency and add the plug to your router or endpoint. For all available options, see: https://github.com/xtian/plug_content_security_policy#usage
plug PlugContentSecurityPolicy,
nonces_for: [
:style_src,
# ...
],
directives: %{
script_src: ~w(https: 'self'),
# ...
}
Using phoenix_live_dashboard with a CSP
Phoenix Live Dashboard supports running with a restrictive Content Security Policy. To use it with a CSP, nonces have to be used. To do this, the nonces have to be set on the plug connection assigns. If you use plug_content_security_policy, the nonces will be set automatically if enabled via nonces_for
.
For detailed configuration options, see:
Phoenix.LiveDashboard.Router.html.live_dashboard/2
live_dashboard "/",
metrics: {AcmeTelemetry, :metrics},
csp_nonce_assign_key: %{
img: :img_src_nonce,
style: :style_src_nonce,
script: :script_src_nonce
}
Alpine.js
A popular stack used in the Elixir ecosystem to create fully functional server-side web experiences is the PETAL stack - Phoenix, Elixir, Tailwind, Alpine.js, Liveview. While using this technology stack, developers can almost entirely forgo the usage of JavaScript when building their applications. Unfortunately at this point in time, there is still a requirement to use some amount of JavaScript in order to build to the modern web - this is usually accomplished via Alpine.js.
As outlined in the Alpine.js developer documentation, “In order for Alpine to be
able to execute plain strings from HTML attributes as JavaScript expressions,
for example x-on:click="console.log()"
, it needs to rely on utilities that
violate the ‘unsafe-eval’ content security policy.
Under the hood, Alpine doesn’t actually use eval()
itself because it’s slow
and problematic. Instead it uses Function declarations, which are much better,
but still violate unsafe-eval
.
In order to accommodate environments where this CSP is necessary, Alpine will
offer an alternate build that doesn’t violate unsafe-eval
, but has a more
restrictive syntax.
At the time of writing, the Alpine.js project has not yet released the aforementioned alternate build that is more restrictive - leaving it up to the developers to either develop around this limitation or build their own version of Alpine.js from the source code.
Cross Site Request Forgery (CSRF)
Cross site request forgery (CSRF) is a type of vulnerability in web applications, where an attacker is able to forge commands from a victim user. For example, consider a social media website that is vulnerable to CSRF. An attacker creates a malicious website aimed at legitimate users. When a victim visits the malicious site, it triggers a POST request in the victim’s browser, sending a message that was written by the attacker. This results in the victim’s account making a post written by the attacker.
Phoenix protects against CSRF by default, through a combination of the
protect_from_forgery
plug, and the CSRF token included in form helpers. Forms
generated by Phoenix include a hidden input named _csrf_token
, which is
included in the state changing HTTP request. Sobelow includes a check,
“UID 5, Config.CSRF: Missing CSRF Protections”, which alerts on router pipelines
missing the protect_from_forgery
plug.
Sobelow also includes a check for “CSRF via Action Reuse”. The typical description of CSRF involves a POST request being triggered without a secure token. If there’s a state changing HTML form making a POST, with a CSRF token, that’s validated by the server, that should be secure. Consider a web application where both a GET and POST request can perform the same state changing action. This is likely not the developer’s intention, but that is the root of most security problems in software. For example:
get "/users", UserController, :new
post "/users", UserController, :new
In this instance, it may be possible to trigger the POST functionality with a
GET
request and query parameters.
Further reading:
- Elixir/Phoenix Security: What is CSRF via Action Reuse?
- Elixir/Phoenix Security: Introduction to Cross Site Request Forgery (CSRF)
Cross-Site WebSocket Hijacking (CSWSH)
A cross-site WebSocket hijacking attack is very similar to a CSRF attack, but it aims to establish a WebSocket connection rather than trigger a classical HTTP request. If successful, the attacker obtains a bidirectional channel with the server in the context of a victim’s session. The extent of the damage the attacker can do depends on the functionality exposed via the WebSocket, but it could potentially allow the attacker to take full control over the user’s account and extract private information.
To protect against CSWSH, Phoenix applies CSRF protections when the socket is
configured to expose the session through the
connect_info
mechanism. If the CSRF
token does not match, as would be expected in a cross-origin scenario, the
session contents is ignored and the socket’s connect callback receives an empty
(nil
) session instead. Any session-based authentication logic should therefore
fail, presumably blocking the CSWSH attack.
Note, however, that the CSRF token verification does not block the connection
itself: it only applies when session information is requested, and it is the
responsibility of the connect callback to close the connection when
authentication fails. Phoenix provides another mechanism to automatically block
connection attempts based on the browser’s Origin
header.
The socket
Endpoint function accepts a
check_origin
parameter to configure which origin values are allowed. By default
it uses the value from the
Endpoint configuration, which in turn
defaults to true
, allowing WebSocket connections only if the origin matches
the Endpoint’s configured hostname.
If the socket is intended to be used in a cross-origin context or from outside a
browser it may be necessary to disable the origin check (check_origin: false
).
In that case make sure the connection authentication is based on a token passed
through a query parameter rather than cookies.
See also Session lifecycle and WebSocket connections
SQL Injection
Preventing SQL Injection in Elixir/Phoenix/Ecto applications
- Use Ecto to build queries. The library has very strong SQL injection
prevention.
- SQL injection vulnerabilities are introduced through the “escape hatch”
provided by Ecto via the
Ecto.Adapters.SQL
function that allows raw SQL input.
- SQL injection vulnerabilities are introduced through the “escape hatch”
provided by Ecto via the
- Ecto vectors for SQL injection:
Repo.query
Repo.query!
Repo.query_many
Repo.query_many!
Ecto.Adapters.SQL.query
Ecto.Adapters.SQL.query!
Ecto.Adapters.SQL.query_many
Ecto.Adapters.SQL.query_many!
Ecto.Adapters.SQL.stream
- When auditing a codebase for these functions, remember a call to
Ecto.Adapters.SQL.query
may be shortened to justquery
via an Elixir import statement. - It is possible to use the above APIs safely, if the query input is provided
via parameters. For example:
https://elixirforum.com/t/ecto-adapters-sql-query-for-sql-query-leads-to-sql-injection-attack/34678/2
- Safe:
Ecto.Adapters.SQL.query(Repo, "SELECT $1", [input_a])
- Unsafe:
Ecto.Adapters.SQL.query(Repo, "SELECT #{input_a}"")
- Unsafe:
q = "SELECT" <> input_a
Ecto.Adapters.SQL.query(Repo, q)
- Safe:
Further Reading:
Denial of Service (DoS)
Atom Exhaustion
Phoenix applications are inherently robust, because they are created with Elixir, running on Erlang’s virtual machine (BEAM). The biggest denial of service (DoS) risk to a Phoenix application is atom exhaustion. This occurs when user input to a Phoenix application results in the creation of new atoms.
The Security Working Group of the Erlang Ecosystem Foundation publishes a guide to prevent atom exhaustion.
The recommendations for Elixir are:
- Use
String.to_existing_atom/1
instead ofString.to_atom/1
- Use
List.to_existing_atom/1
instead ofList.to_atom/1
- Use
Module.safe_concat/1,2
instead ofModule.concat/1,2
- Do not use interpolation to create atoms:
:"new_atom_#{index}"
:'new_atom_#{index}'
~w[row_#{index} column_#{index}]a
- Use the
:safe
option when calling:erlang.binary_to_term/2
on untrusted input (see also Serialisation and deserialisation)
Further reading:
Application Layer (Layer 7) Attacks
If your Phoenix application allows users to perform a computationally expensive operation, which could be used by an attacker for a denial of service attack, it is recommended you rate limit the function. There are several open source Elixir libraries for this:
The usage of such libraries should be implemented according to their respective best practices that also align with expected usage limits of your application. For example, it is strongly encouraged to implement rate limiting on a user login function at a rate that a normal user who is having difficulty logging in would not be penalized yet would still prevent a malicious user from brute forcing the users password.
Web Application Firewalls
While many DoS attacks can be mitigated through techniques such as rate limiting, avoiding exposing computationally expensive functionality, and leveraging Elixir’s full concurrency model - it is worth mentioning that some Distributed Denial of Service (DDoS) attacks can only be mitigated through more appropriate Web Application Firewalls deployed in front of your application.
Client-side enforcement of server-side security
Access controls should always be enforced by the server: it is not enough to hide a “Delete” button from users who do not have permission to delete a resource if the underlying route is not also checking the user’s permissions. Similarly, it is not enough to restrict a list of resources in an index page to only include links to resources the user should be able to access, when a simple change in the identifier in a resource’s URL exposes arbitrary resources.
This is also true for Single Page Applications (SPAs), in which much of the application logic is implemented in the browser: the server that implements the API that allows the client-side code to retrieve and update resources should implement the necessary access controls based on the current user’s permissions.
In a LiveView application it is just a little bit harder to see where these server-side access controls should be implemented. The LiveView guide on security does stress the importance of verifying permissions on the server in the section on events.
So for instance, when the “delete” action’s availability depends on the user’s permissions, and is hidden accordingly…
<:action :let={{id, post}}>
<.link
phx-click={JS.push("delete", value: %{id: post.id}) |> hide("##{id}")}
data-confirm="Are you sure?"
:if={allow_delete?(post, @current_user)}
>
Delete
</.link>
</:action>
…then don’t forget to also implement the permission check in the associated event handler:
@impl true
def handle_event("delete", %{"id" => id}, socket) do
post = Blog.get_post!(id)
if allow_delete?(post, socket.assigns[:current_user]) do
{:ok, _} = Blog.delete_post(post)
{:noreply, stream_delete(socket, :posts, post)}
else
Logger.warn("Unauthorized blog post delete request!")
{:noreply, socket}
end
end