Elixir/Phoenix Security Reference
Ash projects: AshAuthentication has its own strategy/token patterns — use the ash-framework skill. CSRF, XSS, and secret management patterns below still apply.
Quick reference for security patterns in Elixir/Phoenix.
Iron Laws — Never Violate These
- VALIDATE AT BOUNDARIES — Never trust client input. All data through changesets
- NEVER INTERPOLATE USER INPUT — Use Ecto's
^ operator, never string interpolation
- NO String.to_atom WITH USER INPUT — Atom exhaustion DoS. Use
to_existing_atom/1
- AUTHORIZE EVERYWHERE — Check in contexts AND re-validate in LiveView events
- ESCAPE BY DEFAULT — Never use
raw/1 with untrusted content
- SECRETS NEVER IN CODE — All secrets in
runtime.exs from env vars
- LIVEVIEW EVENT PARAMS ARE UNTRUSTED — Users can alter forms, hooks, and every
phx-value-* in DevTools. Validate and authorize against server-side state before acting
Quick Patterns
Timing-Safe Authentication
def authenticate(email, password) do
user = Repo.get_by(User, email: email)
cond do
user && Argon2.verify_pass(password, user.hashed_password) ->
{:ok, user}
user ->
{:error, :invalid_credentials}
true ->
Argon2.no_user_verify() # Timing attack prevention
{:error, :invalid_credentials}
end
end
LiveView Authorization (CRITICAL)
# `id` is client input even when it came from phx-value-id.
# RE-AUTHORIZE IN EVERY EVENT HANDLER
def handle_event("delete", %{"id" => id}, socket) do
post = Blog.get_post!(id)
# Don't trust that mount authorized this action!
with :ok <- Bodyguard.permit(Blog, :delete_post, socket.assigns.current_user, post) do
Blog.delete_post(post)
{:noreply, stream_delete(socket, :posts, post)}
else
_ -> {:noreply, put_flash(socket, :error, "Unauthorized")}
end
end
Rendered LiveView events can expose IDs in HTML and websocket payloads. That is
not automatically a vulnerability: treat IDs as public identifiers, never as
proof of access. Use opaque references only when the identifier itself must not
be disclosed, and still perform server-side authorization.
SQL Injection Prevention
# ✅ SAFE: Parameterized queries
from(u in User, where: u.name == ^user_input)
# ❌ VULNERABLE: String interpolation
from(u in User, where: fragment("name = '#{user_input}'"))
Quick Decisions
What to validate?
- All user input → Ecto changesets
- File uploads → Extension + magic bytes + size
- Paths →
Path.safe_relative/2 for traversal
- Atoms →
String.to_existing_atom/1 only
What to escape?
- HTML output → Auto-escaped by default (
<%= %>)
- User HTML → HtmlSanitizeEx with scrubber
- Never →
raw/1 with untrusted content
Anti-patterns
| Wrong |
Right |
"SELECT * FROM users WHERE name = '#{name}'" |
from(u in User, where: u.name == ^name) |
String.to_atom(user_input) |
String.to_existing_atom(user_input) |
<%= raw @user_comment %> |
<%= @user_comment %> |
| Hardcoded secrets in config |
runtime.exs from env vars |
| Auth only in mount |
Re-auth in every handle_event |
Trusting phx-value-* or hidden IDs |
Load server-side state and authorize it |
References
For detailed patterns, see:
references/authentication.md - phx.gen.auth, MFA, sessions
references/authorization.md - Bodyguard, scopes, LiveView auth
references/input-validation.md - Changesets, file uploads, paths
references/security-headers.md - CSP, CSRF, rate limiting, headers
references/oauth-linking.md - OAuth account linking, token management
references/rate-limiting.md - Composite key strategies, Hammer patterns
references/advanced-patterns.md - SSRF prevention, secrets management, supply chain
1---2name: security-233description: Enforce Elixir/Phoenix security — auth, OAuth, sessions, CSRF, XSS, SQL injection, input validation, secrets. Use when editing auth files, login flows, RBAC, or API keys.4---5
6# Elixir/Phoenix Security Reference
7
8> **Ash projects**: `AshAuthentication` has its own strategy/token patterns — use the `ash-framework` skill. CSRF, XSS, and secret management patterns below still apply.
9
10Quick reference for security patterns in Elixir/Phoenix.
11
12## Iron Laws — Never Violate These
13
141. **VALIDATE AT BOUNDARIES** — Never trust client input. All data through changesets
152. **NEVER INTERPOLATE USER INPUT** — Use Ecto's `^` operator, never string interpolation
163. **NO String.to_atom WITH USER INPUT** — Atom exhaustion DoS. Use `to_existing_atom/1`
174. **AUTHORIZE EVERYWHERE** — Check in contexts AND re-validate in LiveView events
185. **ESCAPE BY DEFAULT** — Never use `raw/1` with untrusted content
196. **SECRETS NEVER IN CODE** — All secrets in `runtime.exs` from env vars
207. **LIVEVIEW EVENT PARAMS ARE UNTRUSTED** — Users can alter forms, hooks, and every `phx-value-*` in DevTools. Validate and authorize against server-side state before acting
21
22## Quick Patterns
23
24### Timing-Safe Authentication
25
26```elixir
27def authenticate(email, password) do
28 user = Repo.get_by(User, email: email)
29
30 cond do
31 user && Argon2.verify_pass(password, user.hashed_password) ->
32 {:ok, user}
33 user ->
34 {:error, :invalid_credentials}
35 true ->
36 Argon2.no_user_verify() # Timing attack prevention
37 {:error, :invalid_credentials}
38 end
39end
40```
41
42### LiveView Authorization (CRITICAL)
43
44```elixir
45# `id` is client input even when it came from phx-value-id.
46# RE-AUTHORIZE IN EVERY EVENT HANDLER
47def handle_event("delete", %{"id" => id}, socket) do
48 post = Blog.get_post!(id)
49
50 # Don't trust that mount authorized this action!
51 with :ok <- Bodyguard.permit(Blog, :delete_post, socket.assigns.current_user, post) do
52 Blog.delete_post(post)
53 {:noreply, stream_delete(socket, :posts, post)}
54 else
55 _ -> {:noreply, put_flash(socket, :error, "Unauthorized")}
56 end
57end
58```
59
60Rendered LiveView events can expose IDs in HTML and websocket payloads. That is
61not automatically a vulnerability: treat IDs as public identifiers, never as
62proof of access. Use opaque references only when the identifier itself must not
63be disclosed, and still perform server-side authorization.
64
65### SQL Injection Prevention
66
67```elixir
68# ✅ SAFE: Parameterized queries
69from(u in User, where: u.name == ^user_input)
70
71# ❌ VULNERABLE: String interpolation
72from(u in User, where: fragment("name = '#{user_input}'"))
73```
74
75## Quick Decisions
76
77### What to validate?
78
79- **All user input** → Ecto changesets
80- **File uploads** → Extension + magic bytes + size
81- **Paths** → `Path.safe_relative/2` for traversal
82- **Atoms** → `String.to_existing_atom/1` only
83
84### What to escape?
85
86- **HTML output** → Auto-escaped by default (`<%= %>`)
87- **User HTML** → HtmlSanitizeEx with scrubber
88- **Never** → `raw/1` with untrusted content
89
90## Anti-patterns
91
92| Wrong | Right |
93|-------|-------|
94| `"SELECT * FROM users WHERE name = '#{name}'"` | `from(u in User, where: u.name == ^name)` |
95| `String.to_atom(user_input)` | `String.to_existing_atom(user_input)` |
96| `<%= raw @user_comment %>` | `<%= @user_comment %>` |
97| Hardcoded secrets in config | `runtime.exs` from env vars |
98| Auth only in mount | Re-auth in every `handle_event` |
99| Trusting `phx-value-*` or hidden IDs | Load server-side state and authorize it |
100
101## References
102
103For detailed patterns, see:
104
105- `references/authentication.md` - phx.gen.auth, MFA, sessions
106- `references/authorization.md` - Bodyguard, scopes, LiveView auth
107- `references/input-validation.md` - Changesets, file uploads, paths
108- `references/security-headers.md` - CSP, CSRF, rate limiting, headers
109- `references/oauth-linking.md` - OAuth account linking, token management
110- `references/rate-limiting.md` - Composite key strategies, Hammer patterns
111- `references/advanced-patterns.md` - SSRF prevention, secrets management, supply chain