In recent years, software security has become a hot topic due to regulatory pressure (NIS2, EU Cyber Resilience Act, etc.). Beyond regulations, software communities and open source maintainers have also put security into focus, because open source libraries are often part of commercial software supply chains. This has reached the Erlang/Elixir ecosystem as well, and that is a good thing.
In this post, the SAFE team takes us through SAFE, Erlang Solutions’ security analysis tool for the BEAM, covering what it does, how it works, and what makes it effective in practice.
The BEAM is secure by default (Up to a point)
The BEAM’s architecture eliminates entire classes of bugs for free. Isolated process memory means processes can’t manipulate each other’s state, they only communicate through messages. Immutable data structures rule out a whole category of aliasing bugs. These are real wins, and they come without any effort from the developer.
But “secure by default” only goes so far. Application-level vulnerabilities, e.g., XSS, SQL injection, CSRF, unsafe deserialization, atom exhaustion, are just as possible in Erlang and Elixir as anywhere else. That’s the gap SAFE exists to cover.
What is SAFE?
SAFE (Security Analysis for Erlang/Elixir) is Erlang Solutions’ static analysis tool for the BEAM. It analyses compiled BEAM files rather than source code, so it works consistently across Erlang, Elixir, and Phoenix, including mixed-language codebases. SAFE is free for open source projects (subject to approval) and commercially available for other use cases.
It is developed in collaboration with academic research on static analysis from Eötvös Loránd University , and are aligned with the security recommendations of the Erlang Ecosystem Foundation (EEF).
SAFE detects a broad range of vulnerabilities, including:
- Cross-Site Scripting (XSS)
- SQL injection
- Command injection
- Remote Code Execution
- Denial of Service (e.g. atom exhaustion)
- Unsafe serialisation
- Cross-Site Request Forgery (CSRF)
- Session hijacking, fixation, and information leakage
- Content Security Policy (CSP) misconfigurations
Data-flow analysis: the core of what makes SAFE different
The central feature that sets SAFE apart is data-flow analysis. Most static analysis tools work by pattern matching, they look for known dangerous function calls and flag them. The problem is that not every call to a dangerous function is actually dangerous. Without understanding the possible values flowing through the code, a tool has no way to tell the difference, and the result is a high rate of false positives.
SAFE takes a different approach. Data-flow analysis tracks what values variables can hold at each point in the program. This information is then used to filter the initial list of vulnerability candidates, eliminating findings where the data can be proven safe, and surfacing only the ones that represent real risk.
The practical impact of this is significant. In our tests across 7 popular open source BEAM projects (~70,000 lines of code), SAFE produced a false positive rate of 7.78% and that number continues to improve as we refine our analysis. We also manually review findings during development to further sharpen the filtering.
How data-flow analysis eliminates false positives
Example 1 — guarded atom creation
A common pattern in Elixir is to convert a binary to an atom only after validating it against an allowlist:
def safe_to_atom(binary, allowed) do
if Enum.member?(allowed, binary), do: String.to_atom(binary)
end
A pattern-matching tool sees String.to_atom/1 and flags it. SAFE’s data-flow analysis traces the possible values of binary at the point of the call and determines that it is always a member of a finite, controlled list so it eliminates the finding entirely.
Example 2 — finite compile-time atom generation
Metaprogramming is common in Elixir. Consider this pattern where atoms are generated at compile time:
@variants [:case_a, :case_b, :case_c]
#
# ...
#
for var <- @variants do
defp unquote(var)() do
env = Application.get_env(:my_app, :environment)
if env == "test" do
unquote(Macro.escape(Module.get_attribute(__MODULE__, :"test_#{var}")))
else
unquote(Macro.escape(Module.get_attribute(__MODULE__, var)))
end
end
end
The number of atoms created here is strictly bounded by the length of @variants, a compile-time constant. SAFE calculates this and correctly determines the atom count is finite hence no vulnerability. A tool without data-flow analysis cannot make this determination.
What SAFE catches in practice
Session management vulnerabilities
Session management vulnerabilities allow attackers to gain unauthorised access to user sessions, which can lead to data theft, unauthorised actions, and account takeover. SAFE detects session hijacking, session fixation, and session information leakage. Session hijacking occurs when an attacker gains access to cookie contents. To prevent this, thehttp_only and secure attributes should both be set to true when setting a cookie. Below is a vulnerable example:
@spec set_cookie(Plug.Conn.t()) :: Plug.Conn.t()
def set_cookie(conn) do
Plug.Conn.put_resp_cookie(conn, "my_cookie", "true",
http_only: false,
max_age: @max_age # an integer
)
end
Session fixation is an attack where a malicious user plants a session ID for a victim to use, then hijacks their account after login. The Plug.Session API provides the configure_session/2 function for renewing the session ID. When this function is misconfigured by setting the renew option to false, session fixation can occur.
Session information leakage can be prevented by encrypting cookie contents. Encryption can be enabled by setting the encryption_salt in Plug.Session. For non-session cookies, encryption can be enabled via the encrypt option in Plug.Conn.put_resp_cookie/4.
Content Security Policy misconfigurations
When it comes to Content Security Policy (CSP), any policy is better than none. Using :put_secure_browser_headers without a custom policy won’t be enough on its own:
plug :put_secure_browser_headers, %{
"content-security-policy": "[Your Policy]"
}
There are also dedicated plugs for this, such as PlugContentSecurityPolicy:
plug PlugContentSecurityPolicy,
SAFE inspects the policy content itself and will flag an overly permissive default. You should define your own policy, keeping it as restrictive as possible to avoid unintentionally whitelisting too much. Beyond that, SAFE checks that all pipelines accepting HTML have CSP protection at all, a gap that is easy to miss during development.
Results from the field
The top three vulnerability types are XSS, DoS, and CSP. Notably, a large share of DoS findings trace back to unguarded String.to_atom/1 calls, a known footgun that is consistently flagged in Erlang/Elixir documentation, yet still appears frequently in practice.
SAFE found a total of 90 vulnerabilities, of which 7 were false positives after manual investigation, a false positive ratio of 7.78%, and an area of active improvement.
The projects tested are anonymized to avoid identification, since the disclosed vulnerabilities may still be present in production systems. While this limits full reproducibility, responsible disclosure takes priority. Contact us at safe@erlang-solutions.com to discuss the methodology in detail.
Closing notes
Security analysis on the BEAM is a genuinely hard problem. The same flexibility that makes Erlang and Elixir so expressive also makes it easy to introduce subtle vulnerabilities without realising it. SAFE is built specifically for this environment, grounded in academic research, and designed to give you signals you can act on rather than noise you have to filter.
If you maintain an open-source project, SAFE is free: reach out to us at safe@erlang-solutions.com and after a short approval process you’ll receive a licence at no cost. For commercial use or a third-party security review of your system, get in touch with the team.
The post SAFE: Bringing Real Static Analysis to the BEAM appeared first on Erlang Solutions.























