Curated library of 38 atomic skills, 7 personas, and 1 orchestrator for Elixir and Phoenix development. Organized by category: fundamentals, phoenix, database, testing, auth, infrastructure, quality, security, integrations, tooling, frameworks, personas, and orchestration. Covers core Elixir patterns, Phoenix LiveView, Ecto, OTP, Oban, testing, security, deployment, real-time, and modern tooling (Req, Swoosh, Cachex, Broadway, Ash).
91
91%
Does it follow best practices?
Impact
91%
1.37xAverage score across 56 eval scenarios
Advisory
Suggest reviewing before use
Use this skill before writing ANY security-sensitive code.
String.to_atom/1 on user input — use String.to_existing_atom/1 or a whitelist case (Atom Table Exhaustion)fragment or raw SQL — use ^value bindings, field/2, or $1/$2 placeholders (SQL Injection)~p"..." verified routes or a whitelist of allowed paths (Open Redirects)raw/1 in HEEx — let auto-escaping run, or sanitize with HtmlSanitizeEx first (Cross-Site Scripting (XSS))user_id, email), never passwords, tokens, or PII (Sensitive Data in Logs)Plug.Crypto.secure_compare/2 — never ==, which leaks length/content via timing (Timing Attacks):protect_from_forgery in the browser pipeline and use <.form>, not raw <form> (CSRF Protection)mix deps.audit && mix hex.audit && mix sobelow before merge — fail CI on any HIGH or CRITICAL finding (Dependency Auditing)See assets/security_checklist.md for a copy-paste pre-merge checklist covering input validation, auth, secrets, and production hardening.
Apply this sequence whenever writing or reviewing security-sensitive code:
mix sobelow --router MyAppWeb.Router — static analysis on your router and controllersmix sobelow --private — check private functions for vulnerabilitiesmix deps.audit && mix hex.audit && mix sobelow before merging❌ Bad — trusting user input:
def index(conn, %{"status" => status}) do
users = Repo.all(from u in User, where: u.status == ^status)
render(conn, "index.html", users: users)
end✅ Good — validate against allowed values:
@allowedStatuses ~w(active inactive pending)
def index(conn, %{"status" => status}) do
if status in @allowedStatuses do
users = Repo.all(from u in User, where: u.status == ^status)
render(conn, "index.html", users: users)
else
put_status(conn, :bad_request)
|> json(%{error: "Invalid status"})
end
end❌ Bad — no authorization check:
def show(conn, %{"id" => id}) do
user = Repo.get!(User, id)
render(conn, "show.html", user: user)
end✅ Good — verify ownership:
def show(conn, %{"id" => id}) do
current_user = conn.assigns.current_user
case Accounts.get_user_for_current_user(current_user, id) do
{:ok, user} -> render(conn, "show.html", user: user)
{:error, :not_found} -> put_status(conn, :not_found) |> json(%{error: "Not found"})
{:error, :unauthorized} -> put_status(conn, :forbidden) |> json(%{error: "Forbidden"})
end
end❌ Bad — user controls the atom:
role = String.to_atom(params["role"])✅ Good — whitelist approach:
case params["role"] do
"admin" -> :admin
"user" -> :user
"moderator" -> :moderator
_ -> {:error, :invalid_role}
endNever interpolate strings into Ecto queries; use ^variable or $1/$2 placeholders.
❌ Bad — string interpolation in fragment:
# NEVER do this — user can inject SQL through field or value
from(u in User, where: fragment("lower(#{field}) = ?", ^value))
from(u in User, where: fragment("#{condition}", []))❌ Bad — using unvalidated input in raw SQL:
# NEVER do this — even with ~s() sigil
Ecto.Adapters.SQL.query(Repo, "SELECT * FROM users WHERE name = '#{name}'", [])✅ Good — parameterized fragment with field/1:
# Safe — field is an atom from schema, value is parameterized
from(u in User, where: fragment("lower(?) = ?", field(u, :status), ^value))✅ Good — parameterized raw SQL:
Ecto.Adapters.SQL.query(Repo, "SELECT * FROM users WHERE id = $1", [id])
Ecto.Adapters.SQL.query(Repo, "SELECT * FROM users WHERE name = $1 AND status = $2", [name, status])✅ Good — Ecto query expressions always safe:
# Ecto query expressions are always parameterized
from(u in User, where: u.status == ^status and u.name == ^name)Never redirect to user-controlled URLs; use ~p"..." or a whitelist.
❌ Bad — user controls redirect destination:
def create(conn, %{"redirect_to" => redirect_to} = params) do
redirect(conn, to: redirect_to)
end✅ Good — use verified routes:
redirect(conn, to: ~p"/dashboard")✅ Good — validate against known paths:
@allowed_redirects ["/dashboard", "/profile", "/settings"]
def create(conn, %{"redirect_to" => redirect_to} = params) do
if redirect_to in @allowed_redirects do
redirect(conn, to: redirect_to)
else
redirect(conn, to: ~p"/dashboard")
end
endAvoid raw/1; sanitize with HtmlSanitizeEx if HTML is required.
❌ Bad — bypasses escaping:
<%= raw(@user_bio) %>✅ Good — let Phoenix auto-escape:
<%= @user_bio %>✅ Good — sanitize if HTML rendering is required:
<%= raw(HtmlSanitizeEx.html5(@user_bio)) %>❌ Bad:
Logger.info("User login", email: email, password: password)
Logger.debug("API call", token: api_token, response: resp)✅ Good:
Logger.info("User login", email: email, user_id: user.id)
Logger.debug("API call", endpoint: url, status: resp.status)Use Plug.Crypto.secure_compare/2 for token comparison; never ==.
❌ Bad — timing-unsafe:
def verify_token(provided_token, stored_token) do
provided_token == stored_token
end✅ Good — constant-time comparison:
def verify_token(provided_token, stored_token) do
Plug.Crypto.secure_compare(provided_token, stored_token)
end# Check for known vulnerabilities in dependencies
mix deps.audit
# Verify package checksums against Hex
mix hex.audit
# Static security analysis on your code
mix sobelow --router MyAppWeb.Router
# Run all three before any merge
mix deps.audit && mix hex.audit && mix sobelow --configSobelow categories:
| Category | Severity |
|---|---|
| Config (hardcoded secrets, insecure config) | HIGH |
| SQL injection | HIGH |
| Remote Code (unsafe eval/apply) | CRITICAL |
| Cross-Site Scripting | HIGH |
| Function Clobbering | MEDIUM |
| Denial of Service (atom exhaustion) | HIGH |
Add to CI pipeline:
# .github/workflows/security.yml
- name: Security Audit
run: |
mix deps.audit
mix hex.audit
mix sobelow --config# mix.exs
defp aliases do
[
"security.check": ["deps.audit", "hex.audit", "sobelow --config"]
]
endInterpretation: Any Sobelow finding of HIGH or CRITICAL severity MUST be fixed before merging. LOW findings should be tracked and addressed within 2 sprints.
Never disable Phoenix's built-in CSRF protection.
# Phoenix forms automatically include CSRF tokens
# <.form> component handles this — never use raw <form> tags
# API pipeline should NOT include :protect_from_forgery
pipeline :api do
plug :accepts, ["json"]
# No :protect_from_forgery — APIs use Bearer tokens instead
end| ❌ Don't | ✅ Do |
|---|---|
String.to_atom(params["role"]) on user input | Whitelist with a case, or use String.to_existing_atom/1 |
fragment("lower(#{field}) = ?", ^value) | fragment("lower(?) = ?", field(u, :status), ^value) |
redirect(conn, to: params["redirect_to"]) | redirect(conn, to: ~p"/dashboard") or a whitelist |
<%= raw(@user_bio) %> in HEEx | <%= @user_bio %>, or raw(HtmlSanitizeEx.html5(@user_bio)) |
Compare tokens with provided == stored | Plug.Crypto.secure_compare(provided, stored) |
Logger.info("login", password: pw, token: t) | Log user_id / email; never secrets or PII |
Repo.get!(User, id) with no ownership check | Authorize in the context before returning the record |
| Predecessor | This Skill | Successor |
|---|---|---|
| phoenix-auth-customization | security-essentials | phoenix-authorization-patterns |
| ecto-essentials | security-essentials | telemetry-essentials |
Companion skills: phoenix-authorization-patterns, phoenix-auth-customization, phoenix-liveview-auth, code-quality.
.tessl-plugin
evals
scenario-1
scenario-2
scenario-3
scenario-4
scenario-5
scenario-6
scenario-7
scenario-8
scenario-9
scenario-10
scenario-11
scenario-12
scenario-13
scenario-14
scenario-15
scenario-16
scenario-17
scenario-18
scenario-19
scenario-20
scenario-21
scenario-22
scenario-23
scenario-24
scenario-25
scenario-26
scenario-27
scenario-28
scenario-29
scenario-30
scenario-31
scenario-32
scenario-33
scenario-34
scenario-35
scenario-36
scenario-37
scenario-38
scenario-39
scenario-40
scenario-41
scenario-42
scenario-43
scenario-44
scenario-45
scenario-46
scenario-47
scenario-48
scenario-49
scenario-50
scenario-51
scenario-52
scenario-53
scenario-54
scenario-55
scenario-56
skills
frameworks
ash-framework
infrastructure
orchestration
elixir-skill-router
personas
phoenix
quality
security
security-essentials
tooling
mix-tasks-generators