Phoenix ships with strong defaults, so the serious flaws in Elixir applications tend to come from the few places those defaults are bypassed: a GET route on a state-changing action, String.to_atom/1 on user input, a distribution cookie that lets anyone on the network run code, or a remote shell exposed to the wrong interface. Elixir runs on the BEAM, the Erlang virtual machine, which brings risks most web security checklists never mention: a finite atom table that crashes the whole VM when full, a clustering protocol in which one compromised node means every node is compromised, and serialised terms that can carry executable functions. This guide covers authentication and sessions in Phoenix, CSRF and WebSocket protections, Sobelow, atom exhaustion, the distribution cookie and EPMD, secrets such as secret_key_base, logging and a deployment baseline. It is for Elixir developers, platform engineers and application security reviewers, and draws on the Phoenix security guide, the Erlang Ecosystem Foundation (EEF) Security Working Group guidance and the Sobelow project as checked in November 2026. General application security practice is in the Application Security Guide; this page stays on the BEAM specifics.
This is an independent guide from NHI Mgmt Group, based on Phoenix's public documentation as of November 2026. It is not sponsored or reviewed by Phoenix. Product features change, so confirm settings against Phoenix's current documentation.
Key takeaways
- Never create atoms from untrusted input. Atoms are never garbage collected and the table defaults to 1,048,576 entries; when it fills, the VM crashes. Use lookup tables or
String.to_existing_atom/1. - The distribution cookie is a root credential. Any process that can reach the distribution port with the cookie can run arbitrary code on every node. Use TLS distribution, bind it to a private interface, or turn it off with
RELEASE_DISTRIBUTION=none. - CSRF protection only covers what you route correctly.
protect_from_forgerychecks POST, PUT, PATCH and DELETE; adding a GET route to the same action reopens it. Sockets needcheck_originleft on. - Cookie sessions cannot be revoked server side. The default store signs but does not encrypt, and logging out does not invalidate a captured cookie. Use the
phx.gen.authtoken approach and setencryption_salt. - Run Sobelow and the Hex audits in CI. Sobelow finds insecure configuration, raw SQL,
raw/1use, atom creation and unsafe deserialisation;mix hex.auditandmix deps.auditcover dependencies.
How identity and access work in Phoenix applications
Phoenix has no built-in user system. Most applications use the mix phx.gen.auth generator, which writes authentication code into the project rather than adding a library, so the team owns and must review it. Requests pass through router pipelines: the default :browser pipeline fetches the session, applies protect_from_forgery and put_secure_browser_headers, while an :api pipeline typically skips sessions and CSRF and relies on tokens.
| Mechanism | Default behaviour | What to change |
|---|---|---|
Session store (Plug.Session, cookie store) | Session state lives in a signed cookie; HttpOnly set by default; Secure set only if Plug sees the request as HTTPS | Set encryption_salt so contents cannot be read; configure TLS offload so Secure is applied |
| Session revocation | Not possible with the cookie store alone: logout clears the browser cookie only. The only global invalidation is rotating secret_key_base or the signing salt, which signs out everyone | Use server-side session records: phx.gen.auth stores a session_token in the user_tokens table and deletes it on logout and password change |
| Session lifetime | phx.gen.auth invalidates sessions after 60 days by default | Shorten for sensitive applications and add an idle timeout checked server side |
| Session fixation | phx.gen.auth calls renew_session/1 on sign in and out | Keep the call in custom flows; use configure_session(conn, renew: true) on privilege changes |
| Authorisation | None built in | Authorise from conn.assigns.current_user, never from IDs in request parameters |
- Mass assignment: Ecto changesets copy only the fields passed to
cast/3. Casting:is_adminor:rolein a public registration changeset is a direct privilege escalation. Keep separate changesets for user and admin edits. - WebSockets and LiveView: a socket is authenticated once when it connects, and event handlers do not normally check whether the session is still valid.
phx.gen.authsubscribes each socket to a per-session channel and broadcastsdisconnecton logout; custom authentication must do the same. See the WebSocket Security Guide.
Secure coding risks specific to Phoenix and the BEAM
The Phoenix security guide and the EEF guidance converge on a short list of functions and patterns to search for in review. Most are safe by default and only become dangerous when developers reach for an escape hatch.
| Risk | Dangerous pattern | Safe alternative |
|---|---|---|
| CSRF | A GET route pointing at an action that changes state (action re-use CSRF); protect_from_forgery removed from a pipeline | State changes only on POST, PUT, PATCH or DELETE through the :browser pipeline. See the CSRF Prevention Guide |
| Cross-site WebSocket hijacking | check_origin: false on a socket that authenticates by session cookie | Keep check_origin (defaults to the endpoint host); if it must be off, authenticate with a token parameter, not cookies |
| Atom exhaustion (denial of service) | String.to_atom/1, List.to_atom/1, Module.concat, interpolated atoms such as :"field_#{x}" on input | Lookup table of allowed values, or String.to_existing_atom/1 and Module.safe_concat; JSON parsers configured to return strings |
| Unsafe deserialisation | :erlang.binary_to_term/2 on untrusted data, even with [:safe], which does not stop executable function terms | JSON or another standard format; if External Term Format is unavoidable, Plug.Crypto.non_executable_binary_to_term/2 |
| Cross-site scripting | User input in Phoenix.HTML.raw/1, HTML built in html/2 or send_resp/3, content type taken from input | Template interpolation, which escapes by default; restrict upload content types |
| SQL injection | Interpolation into Repo.query, Ecto.Adapters.SQL.query or stream | Ecto query syntax; fragment with ^ parameters (interpolation is rejected at compile time); $1 placeholders. See the SQL Injection Prevention Guide |
| Code and command execution | Input reaching Code.eval_string/3, Code.eval_quoted/3, EEx eval_string, System.cmd/3, System.shell/2, :os.cmd/2 | Never evaluate input; pass fixed executables and argument lists |
| Server-side request forgery | URLs built from input; trusting Req base_url or Tesla BaseUrl as a boundary (absolute URLs override them) | Allowlist destinations; block 169.254.169.254 and internal ranges. See the SSRF Prevention Guide |
| CORS | CORSPlug with a broad regex such as ~r/^http.*/ | Explicit origin list |
Monitor erlang:system_info(atom_count) against atom_limit, as the EEF recommends: a count that keeps rising after startup means something is creating atoms at runtime and will eventually take the node down.
Admin roles and privileged access
The most privileged access to a running Elixir system is not an admin page: it is the remote shell and the distribution protocol. bin/my_app remote attaches an IEx shell to the live node, and bin/my_app rpc "EXPR" evaluates code in it. Both work by joining the node over Erlang distribution with the release cookie, so whoever holds the cookie and can reach the port has full control of the node and, in a cluster, of every node.
- Cookie handling: if no cookie is configured,
mix releasewrites a random one toreleases/COOKIEon first assembly, and at runtime theRELEASE_COOKIEenvironment variable takes precedence. The Elixir documentation recommends a long random value such asBase.encode32(:crypto.strong_rand_bytes(40)). Never commit it, never share one cookie across environments, and remember it is baked into every build artefact that containsreleases/COOKIE. - Cookies are not enough on their own: the EEF notes cookie authentication uses challenge and response but has no protection against an active man-in-the-middle, and default distribution traffic is unencrypted. Use TLS distribution with
{verify, verify_peer}, acacertfileand, preferably, mutual TLS. - EPMD exposure: the Erlang Port Mapper Daemon listens on TCP 4369 and answers unauthenticated name lookups, revealing node names and distribution ports. Bind it with
ERL_EPMD_ADDRESSand bind distribution with theinet_dist_use_interfacekernel option to a private cluster network. - Single-node applications: set
RELEASE_DISTRIBUTION=none(the default issname) or bind to loopback only and reach the shell over an SSH tunnel or VPN, never directly. - No distribution between tiers: distribution removes all isolation between nodes, so the EEF advises using it only for horizontal clusters, with HTTP or database interfaces between layers.
- Operational dashboards: Phoenix LiveDashboard exposes process, ETS and request detail. Its README says to put authentication in front of it in production, for example the admin check generated by
phx.gen.auth. - Erlang SSH daemon: Inventory any application that starts
:sshdaemons, apply Erlang/OTP security updates promptly and follow the Erlang/OTP security advisories.
Integrations, API tokens and service accounts
| Secret or machine identity | Where it lives | Control |
|---|---|---|
secret_key_base | Endpoint configuration, read from SECRET_KEY_BASE in config/runtime.exs; generated with mix phx.gen.secret | Unique per environment, in a secrets manager; rotating it invalidates all sessions and signed tokens |
Session signing_salt and encryption_salt | Endpoint session options | Set both; salts are not secret on their own but changing them is a revocation lever |
| Release cookie | RELEASE_COOKIE or releases/COOKIE | Injected at runtime; file permissions restricted; rotated if a build artefact leaks |
| Database and API credentials | config/runtime.exs from environment | Never in config/prod.exs, which is compiled into the release |
| Signed tokens for APIs and email links | Generated with Phoenix.Token or the user_tokens table | Short max_age, single use for reset and confirmation links |
| Hex packages and build tooling | mix.lock, CI runners | Dependency code runs on build servers as well as in production; review additions and updates |
Prefer config/runtime.exs for every secret: values in compile-time config end up inside the release and in any image built from it. Treat outbound API keys used by the application as non-human identities with an owner and a rotation date. See the Secrets Management Guide. For dependencies, the EEF recommends mix hex.outdated for available updates, mix hex.audit for retired package versions and mix deps.audit (the mix_audit project) against the Elixir Security Advisories database, plus an SBOM for wider tooling. See the SBOM Guide.
Logging and detection
- Parameter filtering: Phoenix logs request parameters on every request and by default filters only names containing "password". Extend
config :phoenix, :filter_parameterswith token, secret and personal data fields, or switch to an allow-list. - LiveView leaks: when a socket process crashes, the triggering
Phoenix.Socket.Messageis logged in the termination report and the parameter filter does not apply. Implement theInspectprotocol for sensitive structs and mark Ecto fieldsredact: true. - Crash dumps:
erl_crash.dumpcaptures process state and can contain credentials. Restrict access, keep secrets out of process state where you can, or setERL_CRASH_DUMP_SECONDS=0in the most sensitive environments, accepting the loss of diagnostics. - Security events to emit: sign-in success and failure, password and email changes, session revocation, role changes, CSRF failures (
Plug.CSRFProtection.InvalidCSRFTokenError) and socket origin rejections. - Network signals: connections to TCP 4369 or the distribution port range from outside the cluster network, and any
remoteorrpcsession outside a change window.
Useful measures: Sobelow high and medium confidence findings open on the main branch (target zero), days from an advisory in a dependency to deployment, the share of nodes with distribution bound to a private interface or disabled (target 100%), atom count growth after startup, and the number of secrets read from compile-time configuration (target zero).
Hardening baseline
- Add Sobelow:
{:sobelow, "~> 0.13", only: [:dev, :test], runtime: false}, runmix sobelowlocally, then in CI with--exitat a chosen--thresholdand--format jsonfor reporting. Start athigh, triage, then lower the threshold. Record accepted false positives with--mark-skip-alland--skipso they are reviewed, not silenced. Relevant checks includeConfig.CSRF,Config.CSRFRoute,Config.CSWH,Config.HSTS,Config.CSP,Config.SecretsandDOS.StringToAtom. See the SAST Triage Guide. - Enforce HTTPS: set
force_sslin the production endpoint (HSTS on by default) and, behind a load balancer, injectX-Forwarded-Protoand userewrite_onso Plug setsSecurecookies and builds HTTPS URLs. UsePlug.SSL.configureor the endpointcipher_suiteoption where Phoenix terminates TLS. - Headers: keep
put_secure_browser_headersand pass a Content Security Policy to it. See the Security Headers Guide. - Sessions: set
encryption_salt, use server-side session tokens, shorten lifetimes, and disconnect sockets on revocation. - Secrets: move all secrets to
config/runtime.exsand a secrets manager; generatesecret_key_baseper environment. - Releases: deploy as a release, including only the OTP applications needed; releases use embedded mode, loading code at boot rather than on demand.
- Distribution: disable it, bind it to loopback, or run TLS distribution on a private network with EPMD bound likewise.
- Test and roll back: trial header and CSP changes in report-only mode, roll session changes out with a planned forced sign-in, and keep the previous release on disk so
bin/my_appcan be restarted on it if a change breaks production.
Common misconfigurations
- Distribution open on all interfaces: both EPMD and distribution bind to every interface by default; on a cloud VM with a permissive security group that is remote code execution for anyone who guesses or steals the cookie.
- Cookie in source or image: a hard-coded
:cookieinmix.exsor areleases/COOKIEfile baked into a public container image. - API pipeline reused for browser forms: moving form routes into a pipeline without
protect_from_forgeryto fix a token error. - Dynamic atoms from JSON keys: decoding with atom keys or converting parameter names to atoms to build queries.
- Trusting signed cookies to be private: storing roles or credit decisions in the default session, which any user can decode.
- Secrets in
prod.exs: compile-time configuration copied into every artefact and image layer.
Framework mapping
| Area | NIST SP 800-53 Rev. 5 | MITRE ATT&CK |
|---|---|---|
| Input handling (atoms, SQL, eval, deserialisation) | SI-10 | T1190 Exploit Public-Facing Application; T1499.004 Application or System Exploitation |
| Sessions and CSRF | SC-23 Session Authenticity | T1539 Steal Web Session Cookie |
| Distribution, remote shell and EPMD | AC-3, CM-7, SC-8 | T1021 Remote Services |
| Secrets in configuration | IA-5, SC-12 | T1552.001 Credentials In Files |
| Sobelow and code review | SA-11, SA-15 | Supports prevention of the above |
| Hex dependency audits | SR-3, RA-5 | T1195.002 Compromise Software Supply Chain |
Practitioner checklist
- Grep for
String.to_atom,binary_to_term,raw(,Repo.query,Code.evalandSystem.shell, and justify every hit. - Confirm every state-changing route is non-GET and passes through
protect_from_forgery. - Leave
check_originon for all sockets, or use token authentication where it is off. - Use server-side session tokens, set
encryption_saltand disconnect sockets on logout. - Run Sobelow,
mix hex.auditandmix deps.auditin CI with failing thresholds. - Generate a unique
secret_key_baseand release cookie per environment and inject both at runtime. - Disable distribution or bind it and EPMD to a private network with TLS distribution.
- Protect LiveDashboard and any admin LiveViews with authentication and an admin role check.
- Extend
filter_parameters, redact sensitive Ecto fields and control access to crash dumps.
Standards and references
- Phoenix: Security Considerations
- Erlang Ecosystem Foundation: Secure Coding and Deployment Hardening Guidelines
- Erlang Ecosystem Foundation: Web Application Security Best Practices for BEAM Languages
- NCC Group: Sobelow Security Static Analysis for Phoenix
- Elixir: Mix.Tasks.Release
- OWASP: Application Security Verification Standard 5.0
- NIST: SP 800-53 Rev. 5
Related NHI Mgmt Group resources: Application Security Guide · Secure Coding Guide · CSRF Prevention Guide · WebSocket Security Guide · Secrets Management Guide