agentsclimarketplace

Security essentials

Skill j-morgan6/elixir-phoenix-guide/skills/security-essentials

Use when code touches user input, tokens, redirects, raw SQL, or logging — injection, XSS, atom exhaustion, timing attacks, audit tooling.From its SKILL.md

Install
npx -y skills add j-morgan6/elixir-phoenix-guide --skill security-essentials

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

SKILL.md

7.7 KB, as published. Nobody here has run it

Security Essentials

RULES — Follow these with no exceptions

  1. Never use String.to_atom/1 on user input — atoms are never garbage collected; user-controlled atoms exhaust the atom table and crash the BEAM VM. String.to_existing_atom/1 avoids that risk but still raises ArgumentError on unknown input (unhandled 500) — whitelist with a case instead of either
  2. Never interpolate strings into fragment() or SQL.query() — always use ? parameters for fragments and $1 for raw SQL
  3. Never redirect to user-controlled URLs — validate against a whitelist or use verified routes (~p"...")
  4. Avoid raw/1 in templates — Phoenix auto-escapes for a reason; if HTML is required, sanitize first with a library like HtmlSanitizeEx
  5. Never log sensitive data — passwords, tokens, secrets, API keys, and credentials must never appear in Logger calls
  6. Use Plug.Crypto.secure_compare/2 for token comparison — never ==, which enables timing attacks
  7. Run dependency audits after changes — mix hex.audit checks for retired/deprecated packages (checksum verification against Hex is already automatic via mix.lock); mix deps.audit (requires adding {:mix_audit, "~> 2.1", only: [:dev, :test], runtime: false}) checks for known CVEs; mix sobelow does static security analysis of your code

Atom Table Exhaustion

The BEAM atom table has a fixed limit (default ~1M atoms) and is never garbage collected. If an attacker can create arbitrary atoms, they crash the entire VM.

Bad:

# User controls the atom — creates atoms forever: atom table exhaustion, VM crash
role = String.to_atom(params["role"])

# Never creates atoms, but raises ArgumentError on unknown input (unhandled 500)
status = String.to_existing_atom(params["status"])

Good:

# Whitelist approach — only known values become atoms
case params["role"] do
  "admin" -> :admin
  "user" -> :user
  "moderator" -> :moderator
  _ -> {:error, :invalid_role}
end

# Or keep as strings throughout
def authorize(%{"role" => "admin"}), do: :ok
def authorize(%{"role" => _}), do: {:error, :unauthorized}

SQL Injection

Ecto's query DSL is safe by default. Danger arises with fragment/1 and Ecto.Adapters.SQL.query/3.

Bad:

# String interpolation in fragment — SQL injection
from(u in User, where: fragment("lower(#{field}) = ?", ^value))

# String interpolation in raw SQL
Ecto.Adapters.SQL.query(Repo, "SELECT * FROM users WHERE id = #{id}")

# String concatenation in queries
query = "SELECT * FROM users WHERE name = '" <> name <> "'"

Good:

# Parameterized fragment — safe
from(u in User, where: fragment("lower(?) = ?", field(u, ^field_name), ^value))

# Parameterized raw SQL — safe
Ecto.Adapters.SQL.query(Repo, "SELECT * FROM users WHERE id = $1", [id])

# Ecto query DSL — always safe
from(u in User, where: u.name == ^name) |> Repo.one()

Open Redirects

Redirecting to a user-supplied URL lets attackers craft phishing links that appear to come from your domain.

Bad:

# User controls redirect destination
def create(conn, %{"redirect_to" => redirect_to} = params) do
  # ... create resource ...
  redirect(conn, to: redirect_to)
end

Good:

# Use verified routes
redirect(conn, to: ~p"/dashboard")

# Or validate against known paths
@allowed_redirects ["/dashboard", "/profile", "/settings"]

def create(conn, %{"redirect_to" => redirect_to} = params) do
  # ... create resource ...
  if redirect_to in @allowed_redirects do
    redirect(conn, to: redirect_to)
  else
    redirect(conn, to: ~p"/dashboard")
  end
end

# Phoenix's built-in approach for auth redirects
defp maybe_store_return_to(conn) do
  # Only store relative paths
  return_to = conn.request_path
  if String.starts_with?(return_to, "/") and not String.starts_with?(return_to, "//") do
    put_session(conn, :user_return_to, return_to)
  else
    conn
  end
end

Cross-Site Scripting (XSS)

Phoenix auto-escapes all template output by default. Using raw/1 bypasses this protection.

Bad:

# In HEEx template — bypasses escaping
<%= raw(@user_bio) %>
<%= Phoenix.HTML.raw(@comment_body) %>

Good:

# Let Phoenix auto-escape (default behavior)
<%= @user_bio %>

# If HTML rendering is required, sanitize first
<%= raw(HtmlSanitizeEx.html5(@user_bio)) %>

# Or use Phoenix.HTML.Format for simple formatting
<%= text_to_html(@user_bio) %>

Phoenix's built-in protections (already active):

  • All <%= %> output is HTML-escaped
  • CSRF tokens in forms (<.form> handles this)
  • Content Security Policy headers (add in your endpoint or Plug pipeline)

Sensitive Data in Logs

Logs are stored in plaintext, shipped to third-party services, and often retained for months. Never log secrets.

Bad:

Logger.info("User login", email: email, password: password)
Logger.debug("API call", token: api_token, response: resp)
Logger.error("Auth failed", credentials: credentials, secret: secret)

Good:

Logger.info("User login", email: email, user_id: user.id)
Logger.debug("API call", endpoint: url, status: resp.status)
Logger.error("Auth failed", user_id: user_id, reason: :invalid_credentials)

# Use Logger metadata for request correlation (no secrets)
Logger.metadata(request_id: conn.assigns[:request_id])

Timing Attacks

Standard == comparison short-circuits on the first different byte, leaking information about the secret through response timing.

Bad:

# Timing-unsafe — leaks token value byte by byte
def verify_token(provided_token, stored_token) do
  provided_token == stored_token
end

# Also bad — pattern match is also timing-unsafe
def verify(%{token: token}, token), do: :ok

Good:

# Constant-time comparison — same duration regardless of input
def verify_token(provided_token, stored_token) do
  Plug.Crypto.secure_compare(provided_token, stored_token)
end

# Phoenix already uses this for CSRF and session tokens
# Apply the same principle to your own token comparisons

Dependency Auditing

Run these commands after adding or updating dependencies:

# Check for known CVEs in dependencies (requires mix_audit — see below)
mix deps.audit

# Check for retired/deprecated packages
# (checksum verification against Hex is automatic via mix.lock — no separate step needed)
mix hex.audit

# Static security analysis of your code
mix sobelow

# All three in sequence
mix deps.audit && mix hex.audit && mix sobelow

mix deps.audit requires adding the dependency:

# mix.exs
{:mix_audit, "~> 2.1", only: [:dev, :test], runtime: false}

Add to CI pipeline:

# In mix.exs aliases
defp aliases do
  [
    "security.check": ["deps.audit", "hex.audit", "sobelow --config"]
  ]
end

CSRF Protection

Phoenix includes CSRF protection by default. Don't disable it.

# Phoenix forms automatically include CSRF tokens
# <.form> component handles this — never use raw <form> tags

# If building a JSON API, CSRF is handled differently:
# API pipeline in router.ex should NOT include :protect_from_forgery
pipeline :api do
  plug :accepts, ["json"]
  # No :protect_from_forgery — APIs use Bearer tokens instead
end

See elixir-essentials skill for general Elixir patterns. See phoenix-authorization-patterns skill for access control patterns. See telemetry-essentials skill for secure logging practices.

Keep looking

Skills are one crate of 628,971. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.