security

SKILL.md

Elixir/Phoenix Security Reference

Quick reference for security patterns in Elixir/Phoenix.

Iron Laws — Never Violate These

  1. VALIDATE AT BOUNDARIES — Never trust client input. All data through changesets
  2. NEVER INTERPOLATE USER INPUT — Use Ecto's ^ operator, never string interpolation
  3. NO String.to_atom WITH USER INPUT — Atom exhaustion DoS. Use to_existing_atom/1
  4. AUTHORIZE EVERYWHERE — Check in contexts AND re-validate in LiveView events
  5. ESCAPE BY DEFAULT — Never use raw/1 with untrusted content
  6. SECRETS NEVER IN CODE — All secrets in runtime.exs from env vars

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)

# 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

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
  • PathsPath.safe_relative/2 for traversal
  • AtomsString.to_existing_atom/1 only

What to escape?

  • HTML output → Auto-escaped by default (<%= %>)
  • User HTML → HtmlSanitizeEx with scrubber
  • Neverraw/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

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
Weekly Installs
10
GitHub Stars
59
First Seen
14 days ago
Installed on
opencode10
gemini-cli10
github-copilot10
codex10
amp10
cline10