When Sobelow reports a finding for an Elixir application, there are three key questions the developer should ask:
-
Is this finding a true positive or false positive?
-
What is the security impact of the vulnerability?
-
What is the correct method to fix the vulnerability?
Sobelow is a useful tool, able to analyze the details of a code base much faster than a human. However, it only has access to source code. Sobelow does not understand how the application is being used, and the true security impact of each reported finding. The expertise of a human is required to use Sobelow effectively.
This document provides guidelines for evaluating each possible Sobelow finding. The severity ranking for each finding is subject to debate, and ultimately up to the judgement of the application owner. Guidelines on severity are still useful, for example consider a banking application where Sobelow reports two findings:
Misc.BinToTerm
, Severity: HighDOS.StringToAtom
, Severity: Medium
Both these findings are true positives. The Misc.BinToTerm
finding means the application may be vulnerable to remote code execution, where an attacker could gain the equivalent of production SSH access to the web server. From this foothold, the attacker can issue financial transactions, modify customer records, and further compromise the bank's network. Contrast this finding to the DOS.StringToAtom
finding. An attacker can use this vulnerability to crash the banking server. This is not ideal, and is a security issue, however the attacker does not get read/write/execute access to the server, so the severity is lower.
This guide is sponsored by Paraxial.io, an application security platform for Elixir and Phoenix. Paraxial.io Application Secure manages Sobelow scanning to ensure automated compliance. Enterprise customers can request expert help in fixing vulnerabilities reported by Sobelow.
This document is not considered part of the Sobelow official documentation.
There are three possible severity ratings for each finding:
-
High
-
Medium
-
Low
Note that these ratings are meant as a guideline, and the true impact of each vulnerability is dependent on how the application is being used. For example, a cross site scripting (XSS) issue, where the payload can only be viewed by the user who submitted it, would be classified as low. A XSS issues in a social media website would be high, because many people can view the payload, and it can lead to a worm attack, where each person who views the payload spreads it to their friends. See the MySpace Samy worm for a real world example.
Given the above context, XSS is classified as high severity in these guidelines.
Command Injection vulnerabilities are a result of passing untrusted input to an operating system shell, and may result in complete system compromise.
This is a high severity finding. An attacker can exploit this vulnerability to take over your entire web server, stealing your database, and causing a major data breach incident.
The danger of command injection is that an attacker can send a malicious string which is passed to a call to :os.cmd
. This function requires a charlist, for example:
iex(11)> :os.cmd(user_input)
'CHANGELOG.md\nLICENSE\nREADME.md\nlib\nmix.exs\nmix.lock\ntest\n'
Follow this checklist to determine if user input can reach this function:
-
Start at the call to
:os.cmd(user_input)
in your code base. Can you determine the source of this variable? -
If it is hard-coded in the source code, user input does not change the variable, so the finding is a false-positive.
-
If the variable comes from user input, for example a GET or POST request, the function is vulnerable.
-
If the variable comes from a database, or other data store, you need to determine if it set by a user.
If you are able to provide some input to your application, which changes the variable passed to :os.cmd
, you have verified this finding as a true positive.
-
Consider removing the call to
:os.cmd
. Can you accomplish the same task without using this dangerous function? -
If you must use
:os.cmd
, do not pass arbitrary user input to this function. Each input to this function should be pre-defined, not created dynamically with user input.
Command Injection vulnerabilities are a result of passing untrusted input to an operating system shell, and may result in complete system compromise.
This is a high severity finding. An attacker can exploit this vulnerability to take over your entire web server, stealing your database, and causing a major data breach incident.
The danger of command injection is that an attacker can send a malicious string which is passed to a call to System.cmd
. For example:
iex(16)> System.cmd(user_input, [])
{"CHANGELOG.md\nLICENSE\nREADME.md\nlib\nmix.exs\nmix.lock\ntest\n", 0}
Follow this checklist to determine if user input can reach this function:
-
Start at the call to
System.cmd
in your code base. Can you determine the source of the variable passed to this function? -
If it is hard-coded in the source code, user input does not change the variable, so the finding is a false-positive.
-
If the variable comes from user input, for example a GET or POST request, the function is vulnerable.
-
If the variable comes from a database, or other data store, you need to determine if it set by a user.
If you are able to provide some input to your application, which changes the variable passed to System.cmd
, you have verified this finding as a true positive.
-
Consider removing the call to
System.cmd
. Can you accomplish the same task without using this dangerous function? -
If you must use
System.cmd
, do not pass arbitrary user input to this function. Each input to this function should be pre-defined, not created dynamically with user input.
Content-Security-Policy is an HTTP header that helps mitigate a number of attacks, including Cross-Site Scripting.
This is a low severity finding. Missing CSP is not a vulnerability, it is a layer of defense to stop XSS and data injection attacks. An attacker must exploit a XSS vulnerability that already exists for CSP to be relevant.
Check the HTTP response from your web server for the content-security-policy
header.
Use plug :put_secure_browser_headers
in your pipeline. Documentation on the put_secure_browser_headers
plug functioncan be found here: https://hexdocs.pm/phoenix/Phoenix.Controller.html#put_secure_browser_headers/2
Example policy:
plug :put_secure_browser_headers, %{"content-security-policy" => "default-src 'self'"}
Warning: Note that adding a restrictive CSP header will improve security, but may break your application's JavaScript. Read https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP for more details.
In a Cross-Site Request Forgery (CSRF) attack, an untrusted application can cause a user's browser to submit requests or perform actions on the user's behalf.
This is a medium severity finding. An attacker can create a malicious web page, and when a victim visits the web page, the attacker can force the victim's browser to perform an action in the web application. The attacker cannot leak data from the victim through CSRF, it is a write-only attack. For example, a banking application vulnerable to CSRF where an attacker can force the victim to make a POST request, transferring money to the attacker.
In your Phoenix router, there should be two routes that use the same action, for example:
get "/users/settings/edit_bio", UserSettingsController, :edit_bio
post "/users/settings/edit_bio", UserSettingsController, :edit_bio
Note that both the GET and POST request are sent to the same function, :edit_bio
. A user can update their bio with a POST request, where the POST body contains user[bio]=This+is+some+info+about+my+profile+page
. This is safe, state changing actions should use POST, because they can be protected from CSRF. This same state changing action can also be triggered via a GET request, which is unsafe, because GET requests are always vulnerable to CSRF. For example, if the victim visits the route:
https://potionshop.url/users/settings/edit_bio?user%5Bbio%5D=Hacked+LOL
Their bio will be updated.
The POST request to /users/settings/edit_bio
is not vulnerable to CSRF. Rather, it's the GET request to /users/settings/edit_bio
, which contains the same parameters as the POST request in the URL, which is the source of this vulnerability. If you can issue a GET request that triggers the same functionality as the POST request, this finding is a true positive.
Additional details - https://paraxial.io/blog/action-reuse-csrf
-
Create different controller actions for each route.
-
In the GET request route, do not allow state changing actions for authenticated user associated with a POST request. Examples of state changing actions are transferring money in a banking application, adding an admin user to a business management portal, or creating a new post on a social media website.
In a Cross-Site Request Forgery (CSRF) attack, an untrusted application can cause a user's browser to submit requests or perform actions on the user's behalf.
This is a medium severity finding. An attacker can create a malicious web page, and when a victim visits the web page, the attacker can force the victim's browser to perform an action in the web application.
The root cause of this finding is a pipeline in your application's router file that fetches a session, but does not implement the :protect_from_forgery
plug.
Locate an HTML form, which makes a POST request, matching this pipeline. For example:
<form action="/posts" method="post">
<label for="post_title">Title</label>
<input id="post_title" name="post[title]" type="text">
<label for="post_body">Body</label>
<textarea id="post_body" name="post[body]">
</textarea>
<div>
<button type="submit">Save</button>
</div>
</form>
This form is vulnerable to CSRF because there is no CSRF token. Note that even if there is a CSRF token in the form, such as:
<input name="_csrf_token" type="hidden" value="C3ceAlcYFxhHPC8WAnUfNCMsARUGJgZ5m9Rd5ZDG-HDVDDTMn_gHg_e8">
The form may still be vulnerable if the plug :protect_from_forgery
is not present.
To verify if the vulnerability is present, create a new file, poc.html
, and enter the following:
<form action="https://localhost:4000/posts" method="post" name="csrf_attack" style="">
<input id="post_title" name="post[title]" value="Hacked">
<input id="post_body" name="post[body]" value="Hacked by Dogs">
<div>
<button type="submit">Save</button>
</div>
</form>
Ensure you are logged into the vulnerable application with a valid session, then open poc.html
in the same web browser. If submitting this form performs the action in your account, in this case creating a post, the vulnerability exists.
-
Ensure you are using the
:protect_from_forgery
plug in pipelines that fetch a session. Even if the HTML form has a CSRF token, the vulnerability still exists if the backend application is not checking if the token is valid.:protect_from_forgery
performs the check. -
The HTML form should be created with a Phoenix helper, such as
form_for
, because it automatically includes the CSRF token - https://hexdocs.pm/phoenix_html/Phoenix.HTML.Form.html#form_for/4 If the matching form does not have a CSRF token, the vulnerability is not fixed.
Websocket connections are not bound by the same-origin policy. Connections that do not validate the origin may leak information to an attacker.
This is a medium severity vulnerability. Exploiting CSWH requires an attacker to setup a malicious website, then get the victim to browse to the site, while also logged into their current session. CSWH does allow the attacker to take over the victim's account, the requirement for user interaction reduces the severity.
Details on CSWH - https://christian-schneider.net/CrossSiteWebSocketHijacking.html
Example of a bad endpoint:
defmodule PhoenixWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :phoenix
socket("/socket", PhoenixInternalsWeb.UserSocket,
websocket: [check_origin: false],
longpoll: false
)
end
Example of a good endpoint:
defmodule PhoenixWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :phoenix
socket("/socket", PhoenixInternalsWeb.UserSocket,
websocket: true,
longpoll: false
)
end
Ensure :check_origin
is enabled. It defaults to true.
Phoenix Docs - https://hexdocs.pm/phoenix/Phoenix.Endpoint.html#socket/3
By default, Phoenix HTTP responses contain a number of secure HTTP headers that attempt to mitigate XSS, click-jacking, and content-sniffing attacks.
Missing Secure HTTP Headers is flagged by sobelow
when a pipeline accepts "html" requests, but does not implement the :put_secure_browser_headers
plug.
This is a low severity finding. Missing secure browser headers is not a vulnerability. Having these headers is considered a best practice.
Most Phoenix applications set these headers by default. This finding is triggered when a router pipeline accepts html
requests, but does not implement the :put_secure_browser_headers
plug.
Check the response of a request in this pipeline for the headers:
referrer-policy
x-frame-options
x-content-type-options
x-download-options
x-permitted-cross-domain-policies
https://hexdocs.pm/phoenix/Phoenix.Controller.html#put_secure_browser_headers/2
Add the plug :put_secure_browser_headers
to the pipeline.
The HTTP Strict Transport Security (HSTS) header helps defend against man-in-the-middle attacks by preventing unencrypted connections.
This is a low severity finding. The HSTS header is used to defend against man-in-the-middle attacks by preventing unencrypted connections.
Check your config file for:
config :havana, HavanaWeb.Endpoint,
force_ssl: [hsts: true]
Remember that Sobelow is limited to your Phoenix application code. Your deployed application may be using HSTS correctly, due to a server level configuration. Use https://www.ssllabs.com/ssltest/ to verify your deployed settings.
https://hexdocs.pm/phoenix/using_ssl.html#hsts
https://hexdocs.pm/plug/Plug.SSL.html
Set the following config:
config :havana, HavanaWeb.Endpoint,
force_ssl: [hsts: true]
Replace "Havana" with your application name. Use https://www.ssllabs.com/ssltest/ to verify your deployed settings.
Without HTTPS, attackers in a privileged network position can intercept and modify traffic. Sobelow detects missing HTTPS by checking the prod configuration.
This is a high severity finding. Using HTTPS is a requirement if your application handles user data.
This finding is often a false positive, because HTTPS configuration may be set at a different layer in the application stack. For example, your web server may be configured to force HTTPS.
Test if your application serves traffic from https://
and https://
. If you are able to send data to your server over https://
, this is a true positive.
Configure your Phoenix application to use HTTPS - https://hexdocs.pm/phoenix/using_ssl.html#content
This is a medium severity finding. Hard coding secrets in source code is not recommended.
Sobelow checks for configuration variables such as secret_key_base
, password
, and secret
with a matching string. Read the finding, and determine if the value stored in source code is a true secret.
The best practice for secrets is to store them as environment variables.
In Elixir, atoms are not garbage collected. As such, if user input is used to create atoms (as in :"foo\#{bar}"
, or in :erlang.binary_to_atom
), it may result in memory exhaustion. Prefer the String.to_existing_atom
function for untrusted user input.
This is a medium severity finding. It does not allow the attacker to access private data, or performed unauthorized actions. Atom DoS allows an attacker to trigger a crash of the Erlang virtual machine. There are two possible outcomes:
-
The application crashes and restarts. There will be some downtime during the restart, but overall the impact will be low.
-
The application crashes and remains down. This is a higher severity incident.
The behavior of your application depends on the deployment environment. Instructions for testing your environment - https://paraxial.io/blog/atom-dos-impact
Find the line of code where the atom is being created. Is the atom created from user input? For example:
:new_atom_#{a}
Can the variable a
be set through user input? If it can be, this is a true positive.
Do not create new atoms at runtime. Restructure your code so that atoms do not need to be created from user input.
https://erlef.github.io/security-wg/secure_coding_and_deployment_hardening/atom_exhaustion
In Elixir, atoms are not garbage collected. As such, if user input is passed to the List.to_atom
function, it may result in memory exhaustion. Prefer the List.to_existing_atom
function for untrusted user input.
This is a medium severity finding. It does not allow the attacker to access private data, or performed unauthorized actions. Atom DoS allows an attacker to trigger a crash of the Erlang virtual machine. There are two possible outcomes:
-
The application crashes and restarts. There will be some downtime during the restart, but overall the impact will be low.
-
The application crashes and remains down. This is a higher severity incident.
The behavior of your application depends on the deployment environment. Instructions for testing your environment - https://paraxial.io/blog/atom-dos-impact
Find the line of code where the atom is being created. Is the atom created from user input? For example:
List.to_atom(a)
Can the variable a
be set through user input? If it can be, this is a true positive.
Do not create new atoms at runtime. Restructure your code so that atoms do not need to be created from user input.
https://erlef.github.io/security-wg/secure_coding_and_deployment_hardening/atom_exhaustion
In Elixir, atoms are not garbage collected. As such, if user input is passed to the String.to_atom
function, it may result in memory exhaustion. Prefer the String.to_existing_atom
function for untrusted user input.
This is a medium severity finding. It does not allow the attacker to access private data, or performed unauthorized actions. Atom DoS allows an attacker to trigger a crash of the Erlang virtual machine. There are two possible outcomes:
-
The application crashes and restarts. There will be some downtime during the restart, but overall the impact will be low.
-
The application crashes and remains down. This is a higher severity incident.
The behavior of your application depends on the deployment environment. Instructions for testing your environment - https://paraxial.io/blog/atom-dos-impact
Find the line of code where the atom is being created. Is the atom created from user input? For example:
String.to_atom(a)
Can the variable a
be set through user input? If it can be, this is a true positive.
Do not create new atoms at runtime. Restructure your code so that atoms do not need to be created from user input.
https://erlef.github.io/security-wg/secure_coding_and_deployment_hardening/atom_exhaustion
If user input is passed to Erlang's binary_to_term
function it may result in memory exhaustion or code execution. Even with the :safe
option, binary_to_term
will deserialize functions, and shouldn't be considered safe to use with untrusted input.
This is a high severity finding. Unsafe usage of binary_to_term
can lead to a remote code execution vulnerability, which allows an attacker to take over your web server.
Is user input being passed to binary_to_term
? For example:
:erlang.binary_to_term(user_input, [:safe])
The [:safe]
option is misleading, this function is vulnerable. If user input is being passed to binary_to_term
, this is a true positive.
Additional details - https://paraxial.io/blog/elixir-rce
-
Do not pass user input to
:erlang.binary_to_term/2
if you can avoid it. -
Use
Plug.Crypto.non_executable_binary_to_term
instead.
https://hexdocs.pm/plug_crypto/Plug.Crypto.html#non_executable_binary_to_term/2
This is a high severity finding. Calling the Code
functions eval_string
, eval_file
, or eval_quoted
on external user input may allow an attacker to take over your web server.
Is user input being passed to the function that Sobelow flagged? For example:
Code.eval_string(user_input)
If the function is receiving user input, this is a true positive.
Do not allow user input to reach the Code
functions eval_string
, eval_file
, or eval_quoted
. Input to these functions should never come from the network.
https://hexdocs.pm/elixir/Code.html
If user input is passed to EEx eval functions, it may result in arbitrary code execution. The root cause of these issues is often directory traversal.
This is a high severity finding. Calling the EEx functions eval_string
and eval_file
on external user input may allow an attacker to take over your web server.
Is user input being passed to the function that Sobelow flagged? For example:
> user_input = "<%= 2 + 3 %>"
"<%= 2 + 3 %>"
> EEx.eval_string(user_input)
"5"
If the function is receiving user input, this is a true positive.
Do not allow user input to reach the EEx
functions eval_string
and eval_file
. Input to these functions should never come from the network.
https://hexdocs.pm/eex/1.14/EEx.html
This is a high severity finding. SQL injection can be used by an attacker to run authorized database commands, leading to data being stolen, modified, or deleted.
Below is an example of code that is vulnerable to SQL injection:
def e_get_fruit(min_q) do
q = """
SELECT f.id, f.name, f.quantity, f.secret
FROM fruits AS f
WHERE f.quantity > #{min_q} AND f.secret = FALSE
"""
{:ok, %{rows: rows}} =
Ecto.Adapters.SQL.query(Repo, q)
end
The key line is WHERE f.quantity > #{min_q} AND f.secret = FALSE
, where min_q
is user input. You should never construct an SQL query from user input. Rather, external input should be passed as a parameter to the query. For example:
Ecto.Adapters.SQL.query(MyRepo, "SELECT $1::integer + $2", [user_in_a, user_in_b])
is not vulnerable, because the user input (user_in_a, user_in_b)
is being passed as parameters to the SQL query.
Additional details - https://paraxial.io/blog/sql-injection
Ensure that all user input is passed as a parameter to the query
function.
Not safe:
Ecto.Adapters.SQL.query(Repo, "SELECT * FROM potions WHERE name = #{user_input}")
Not safe:
Ecto.Adapters.SQL.query(Repo, "SELECT * FROM potions WHERE name = " <> user_input)
Safe:
Ecto.Adapters.SQL.query(Repo, "SELECT * FROM potions WHERE name = $1", [user_input])
This is a high severity finding. SQL injection can be used by an attacker to run authorized database commands, leading to data being stolen, modified, or deleted.
Below is an example of code that is vulnerable to SQL injection:
def e_get_fruit(min_q) do
q = """
SELECT f.id, f.name, f.quantity, f.secret
FROM fruits AS f
WHERE f.quantity > #{min_q} AND f.secret = FALSE
"""
Ecto.Adapters.SQL.stream(Repo, q)
end
The key line is WHERE f.quantity > #{min_q} AND f.secret = FALSE
, where min_q
is user input. You should never construct an SQL query from user input. Rather, external input should be passed as a parameter to the query. For example:
Ecto.Adapters.SQL.stream(MyRepo, "SELECT $1::integer + $2", [user_in_a, user_in_b])
is not vulnerable, because the user input (user_in_a, user_in_b)
is being passed as parameters to the SQL query.
Additional details - https://paraxial.io/blog/sql-injection
Ensure that all user input is passed as a parameter to the query
function.
Not safe:
Ecto.Adapters.SQL.stream(Repo, "SELECT * FROM potions WHERE name = #{user_input}")
Not safe:
Ecto.Adapters.SQL.stream(Repo, "SELECT * FROM potions WHERE name = " <> user_input)
Safe:
Ecto.Adapters.SQL.stream(Repo, "SELECT * FROM potions WHERE name = $1", [user_input])
This is a high severity finding. If user input is passed to a File
function, an attacker may be able to read unauthorized files, such as ../config/prod.secrets.exs
, and make unauthorized changes to the filesystem.
-
Start at the call to the
File
function in your code base. Can you determine the source of this variable? -
If it is hard-coded in the source code, user input does not change the variable, so the finding is a false-positive.
-
If the variable comes from user input, for example a GET or POST request, the function is vulnerable.
-
If the variable comes from a database, or other data store, you need to determine if it set by a user.
If you are able to provide some input to your application, which changes the variable passed to the File
function, you have verified this finding as a true positive.
Do not pass user input to File
functions. The input should be system generated, for example:
%Plug.Upload{filename: filename, path: path} = upload
When upload
is set by the user, it is not safe to pass the filename
variable. The path
variable is generated by plug, for example:
/var/folders/0m/d5lzvxvs181cl_f5m1x3wrx40000gn/T/plug-1681/multipart-1681411044-723629749623759-3
and is safe to pass as a variable.
If you must combine user input to get a path, make sure it's sanitized by Path.safe_relative/1
before passing to File
functions.
This is a high severity finding. If user input is passed to send_download
, an attacker may be able to read unauthorized files, such as ../config/prod.secrets.exs
, and make unauthorized changes to the filesystem.
Consider the example function in a Phoenix controlelr:
def user_pfp(conn, %{"file_name" => file_name}) do
send_download(conn, {:file, file_name})
end
When file_name
is controlled by the user, this is a true positive.
-
Start at the call to the
send_download
function in your code base. Can you determine the source of the function input? -
If the function input is hard-coded in source, it is a false-positive.
-
If the variable comes from user input, for example a GET or POST request, the function is vulnerable.
-
If the variable comes from a database, or other data store, you need to determine if it set by a user.
If you are able to provide some input to your application, which changes the variable passed to the send_download
function, you have verified this finding as a true positive.
Do not pass user input to send_download
. Inputs to send_download
should be pre-defined in the source code, not created dynamically by user input.
This is a high severity finding. If user input is passed to send_file
, an attacker may be able to read unauthorized files, such as ../config/prod.secrets.exs
, and make unauthorized changes to the filesystem.
Consider the example function in a Phoenix controlelr:
def user_pfp(conn, %{"file_name" => file_name}) do
send_file(conn, 200, file_name)
end
When file_name
is controlled by the user, this is a true positive.
-
Start at the call to the
send_file
function in your code base. Can you determine the source of the function input? -
If the function input is hard-coded in source, it is a false-positive.
-
If the variable comes from user input, for example a GET or POST request, the function is vulnerable.
-
If the variable comes from a database, or other data store, you need to determine if it set by a user.
If you are able to provide some input to your application, which changes the variable passed to the send_file
function, you have verified this finding as a true positive.
Do not pass user input to send_file
. Inputs to send_file
should be pre-defined in the source code, not created dynamically by user input.
This is a high severity vulnerability. An attacker can add themselves as an admin user by exploiting coherence. https://github.com/advisories/GHSA-mrq8-53r4-3j5m
Affected versions
< 0.5.2
Patched versions
0.5.2
This is a high severity vulnerability in Plug, "Arbitrary Code Execution in Cookie Serialization". https://github.com/advisories/GHSA-5v4m-c73v-c7gq
Affected versions
< 1.0.4
>= 1.1.0, < 1.1.7
>= 1.2.0, < 1.2.3
>= 1.3.0, < 1.3.2
Patched versions
1.0.4
1.1.7
1.2.3
1.3.2
This is a high severity finding. Ecto 2.2.0
does not enforce the is_nil
requirement. For an example of why this is dangerous, see this example from Jose Valim:
Imagine you write this query:
from User, where: [api_token: ^params["token"]], limit: 1
Now if someone passes no token, you will accidentally login as any of the users without a token.
https://elixirforum.com/t/why-does-ecto-require-the-use-of-is-nil-1/49241
https://github.com/advisories/GHSA-4r2f-6fm9-2qgh
Affected versions
= 2.2.0
Patched versions
2.2.1
This is a high severity vulnerability in Plug, "Header Injection". https://github.com/advisories/GHSA-9h73-w7ch-rh73
Affected versions
< 1.0.6
>= 1.1.0, < 1.1.9
>= 1.2.0, < 1.2.5
>= 1.3.0, < 1.3.5
Patched versions
1.0.6
1.1.9
1.2.5
1.3.5
This is a high severity vulnerability in Plug, "Null Byte Injection in Plug.Static". https://github.com/advisories/GHSA-2q6v-32mr-8p8x
Affected versions
< 1.0.4
>= 1.1.0, < 1.1.7
>= 1.2.0, < 1.2.3
>= 1.3.0, < 1.3.2
Patched versions
1.0.4
1.1.7
1.2.3
1.3.2
This is a low severity vulnerability in Phoenix, "Arbitrary URL Redirect". https://github.com/advisories/GHSA-cmfh-8f8r-fj96
"An attacker can use this vulnerability to aid in social engineering attacks. The most common use would be to create highly believable phishing attacks."
Affected versions
< 1.0.6
>= 1.1.0, < 1.1.8
>= 1.2.0, < 1.2.3
Patched versions
1.0.6
1.1.8
1.2.3
If an attacker is able to set arbitrary content types for an HTTP response containing user input, the attacker is likely to be able to leverage this for cross-site scripting (XSS).
For example, consider an endpoint that returns JSON with user input:
{"json": "user_input"}
If an attacker can control the content type set in the HTTP response, they can set it to "text/html" and update the JSON to the following in order to cause XSS:
{"json": "<script>alert(document.domain)</script>"}
This is a high severity finding. XSS can lead to user account compromise and a malicious worm spreading via JavaScript. See the MySpace Samy worm for a real world example.
Consider a file upload function in a Phoenix application, where the content-type
of the uploaded image is set by the user.
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
view_photo
is vulnerable to XSS, because an attacker can upload an HTML document, for example:
<script>alert(1)</script>
With the content-type text/html
. When a user visits the page for the uploaded file, the attacker controlled JavaScript will execute.
Additional details - https://paraxial.io/blog/xss-phoenix
Do not allow users to upload HTML documents, which are then shown to users. If you are implementing a file upload system, where only images are expected, do not allow content-type
to be set by users. Restrict the allowed content-type
values to a pre-defined list, for example image/jpeg
, image/png
, etc.
This is a high severity finding. User input should not be passed to the Phoenix.Controller.html/2
function, due to the risk of XSS - https://hexdocs.pm/phoenix/Phoenix.Controller.html#html/2
Are you passing user input to the Phoenix.Controller.html/2
function? Consider an example Phoenix controller:
def html_resp(conn, %{"i" => i}) do
html(conn, "<html><head>#{i}</head></html>")
end
This function is vulnerable to XSS, because user input is being passed directly into the HTML document.
Additional details - https://paraxial.io/blog/xss-phoenix
Use the Phoenix.Controller.render/3
function, which is the standard way to handle user input in HTML documents in Phoenix. The render
function is the standard pattern seen in Phoenix applications, because it protects against XSS by default.
https://hexdocs.pm/phoenix/Phoenix.Controller.html#render/3
This is a high severity finding. User input should not be passed to the Phoenix.HTML.raw
function, due to the risk of XSS - https://hexdocs.pm/phoenix_html/Phoenix.HTML.html
Consider the following code:
lib/cross_web/templates/page/render_b.html.eex
<h2>User input (vulnerable due to Phoenix.HTML.raw/1): </h2>
<%= raw @i %>
The i
variable is controlled by user input, and is being passed to the raw
function. Submit a request that sets i
to <script>alert(1)</script>
, and see how the alert box is rendered. If external user input results in JavaScript being executed, this is a true positive.
If the variable passed to raw
is not controlled by the user, this is a false positive.
Do not pass user input to the raw
function. Ideally you should avoid using raw
, but if you must, ensure that data created at runtime from user input is not passed to raw
.
This is a high severity finding. XSS can lead to user account compromise and a malicious worm spreading via JavaScript. See the MySpace Samy worm for a real world example.
In Phoenix you can pass HTML directly to send_resp
.
def send_resp_html(conn, %{"i" => i}) do
conn
|> put_resp_content_type("text/html")
|> send_resp(200, "#{i}")
end
Note that an attacker can set i
to <script>alert(1)</script>
. However, the above example is unlikely to be seen in real code.
Consider a file upload function in a Phoenix application, where the content-type
of the uploaded image is set by the user.
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
view_photo
is vulnerable to XSS, because an attacker can upload an HTML document, for example:
<script>alert(1)</script>
With the content-type text/html
. When a user visits the page for the uploaded file, the attacker controlled JavaScript will execute.
Additional details - https://paraxial.io/blog/xss-phoenix
Consider how user input is being passed to send_resp
. If user input can be used to build HTML elements on the page, the function is vulnerable.
Use the Phoenix.Controller.render/3
function, which is the standard way to handle user input in HTML documents in Phoenix. The render
function is the standard pattern seen in Phoenix applications, because it protects against XSS by default.