Elixir
Most Elixir applications use Ecto with
ecto_sql and a database
driver such as Postgrex
(PostgreSQL), MyXQL (MySQL/MariaDB), or
ecto_sqlite3 (SQLite).
Ecto's query DSL and Repo.query/2 and Repo.query/3 (the latter when you pass
options) are built around bound parameters: values are sent separately from
the SQL text so the database never parses user data as part of the statement
structure.
Dangerous patterns
Never build SQL strings from user input, whether through interpolation or concatenation:
# Do NOT do this - string interpolation, vulnerable to injection.
sql = "UPDATE people SET name = '#{name}' WHERE id = #{id}"
Repo.query!(sql, [])
# Do NOT do this either - string concatenation, same risk.
sql = "SELECT * FROM people WHERE name = '" <> name <> "'"
Repo.query!(sql, [])
Both patterns hand the database one executable string. If name or id comes
from a user, an attacker can close the string literal and append arbitrary SQL.
Ecto Query DSL
The safest path is Ecto's query DSL, which uses the pin operator ^ to
mark values as bound parameters -never as literal SQL text.
import Ecto.Query
from(p in Person, where: p.name == ^name) |> Repo.all()
from(p in Person, where: p.id == ^id) |> Repo.one()
In these examples, Repo is your application's Ecto.Repo
module, usually aliased as Repo in contexts that use MyApp.Repo. Where a
fully-qualified name is clearer, MyApp.Repo is used instead.
For inserts and updates, use changesets and the corresponding Repo
functions. Ecto builds the parameterized query internally -no SQL is exposed to
user-supplied data:
%Person{}
|> Person.changeset(%{name: name, age: age})
|> Repo.insert()
person
|> Person.changeset(%{name: name})
|> Repo.update()
The pin operator works anywhere a value appears in a query expression:
from(p in Person,
where: p.age > ^min_age,
limit: ^page_size
)
|> Repo.all()
IN clauses
Pin a list directly to generate a safe IN predicate:
ids = [1, 2, 3]
from(p in Person, where: p.id in ^ids) |> Repo.all()
Ecto generates one bind parameter per element automatically.
Dynamic queries
When the query shape depends on runtime conditions -for example, applying only
the filters the user actually provided -use
Ecto.Query.dynamic/2
to build the where expression incrementally. All values remain bound
parameters:
defp build_filters(params) do
Enum.reduce(params, dynamic(true), fn
{:name, name}, acc -> dynamic([p], ^acc and p.name == ^name)
{:age, age}, acc -> dynamic([p], ^acc and p.age > ^age)
_, acc -> acc
end)
end
from(p in Person, where: ^build_filters(params)) |> Repo.all()
This eliminates the temptation to assemble SQL strings when the set of filters is unknown at compile time.
If params is a Phoenix request map, keys are usually strings ("name",
"age"), not atoms -normalize to the shape your Enum.reduce clauses expect
(for example with Enum.map / Map.new or by matching on string keys in the
fn). Otherwise filters can silently not apply because they fall through to
the catch-all clause.
Raw SQL
When you need SQL that the DSL cannot express, keep the query text static and
pass all values in the parameter list. With the PostgreSQL adapter
($1, $2, ...):
Repo.query!("UPDATE people SET name = $1 WHERE id = $2", [name, id])
Repo.query!("SELECT * FROM people WHERE name = $1", [name])
With the MySQL/MariaDB adapter (? for every parameter):
Repo.query!("UPDATE people SET name = ? WHERE id = ?", [name, id])
Repo.query!("SELECT * FROM people WHERE name = ?", [name])
Placeholder syntax is determined by the adapter; ecto_sqlite3 also uses ?.
Check your adapter's documentation when writing SQL by hand.
Repo.query!/2 and Repo.query!/3 raise on error; Repo.query/2 and
Repo.query/3 return {:ok, result} or {:error, reason}.
Ecto.Adapters.SQL.query/4 is an alternative that names the repo explicitly:
Ecto.Adapters.SQL.query(MyApp.Repo, "SELECT * FROM people WHERE id = $1", [id])
fragment/1
When you need a raw SQL expression inside an Ecto query, use
fragment/1. Pass
runtime values through ? placeholders -not through Elixir string
interpolation:
# WRONG - interpolation inside the fragment string is injection-vulnerable.
from(u in User, where: fragment("status = '#{status}'"))
# Correct - ? placeholder keeps the value out of the SQL text.
from(u in User, where: fragment("status = ?", ^status))
Schema field references are always safe because Ecto knows they come from your schema, not user input:
from(u in User, where: fragment("lower(?) = lower(?)", u.email, ^email))
Dynamic identifiers
Table names and column names cannot be sent as bind parameters; the database
requires them in the query text itself. If a column name must vary at runtime
(for example, a user-chosen sort column), validate it against a compile-time
allowlist, then use field/2
to reference it safely:
@sortable_fields ~w(name age inserted_at)
defp order_by_field(query, col_str) when col_str in @sortable_fields do
col = String.to_existing_atom(col_str)
order_by(query, [p], asc: field(p, ^col))
end
defp order_by_field(query, _), do: query
Use String.to_existing_atom/1, not String.to_atom/1. String.to_atom/1
creates a new atom for any input string, which can exhaust the atom table (a
denial-of-service risk) and lets callers name atoms your code never defined.
String.to_existing_atom/1 raises for unknown atoms, making the guard
when col_str in @sortable_fields the real safety gate: it only allows strings
that correspond to atoms already present in the runtime. field/2 then
references the schema field by that atom, keeping the identifier out of any raw
SQL string. In practice, every string that passes the guard will convert without
error because Ecto schema field names are registered as atoms when the module is
compiled.
The same idea extends to sort direction. Do not interpolate "asc" or
"desc" into a SQL string - validate against a fixed list and convert:
@sort_directions ~w(asc desc)
defp order_by_field(query, col_str, dir_str) when col_str in @sortable_fields and dir_str in @sort_directions do
col = String.to_existing_atom(col_str)
dir = String.to_existing_atom(dir_str)
order_by(query, [p], [{^dir, field(p, ^col)}])
end
defp order_by_field(query, _, _), do: query
LIKE wildcards
Parameterization prevents SQL injection but does not neutralize the special
characters % (match any sequence) and _ (match any single character) inside
LIKE patterns. A user who submits % as a search term can match every row.
PostgreSQL: unless you add an ESCAPE clause, backslash is not special
in LIKE, so patterns that rely on \% or \_ will not behave the way many
other databases imply. Pick an escape character that you forbid or normalize in
input (here !), double it when it appears literally, prefix wildcards with it,
then declare ESCAPE '!' in the SQL:
safe_term =
input
|> String.replace("!", "!!")
|> String.replace("%", "!%")
|> String.replace("_", "!_")
search = "%" <> safe_term <> "%"
from(u in User, where: fragment("? LIKE ? ESCAPE '!'", u.name, ^search))
Other engines use different default rules for LIKE and escaping; read your
database's documentation instead of assuming backslash escapes wildcards.
Ecto's ilike/2 is case-insensitive matching with the same % and _
wildcard semantics -use the same escaping approach when you need literal
substring matches. ilike/2 is PostgreSQL-only; on other databases, use
like/2 combined with a database-level case-folding strategy (e.g. lower()
via fragment).