Security

Where SQL injection still lives in Rails

Active Record parameterises almost everything. The gaps are specific, well known, and exactly where people write raw fragments.

17 June 2026 3 min read Mohammad Aaquib Jawed

Active Record makes the common cases safe by default. where(email: params[:email]) binds a parameter; there is no string being assembled and nothing to escape.

Which is why the vulnerabilities that do exist cluster in a small number of well-defined escape hatches.

String interpolation into a condition

The canonical mistake is building a condition with interpolation instead of binding: putting params[:name] directly inside the SQL fragment rather than passing it as a parameter.

Use the array or hash form — where("name = ?", params[:name]) or where(name: ...) — and the value is bound. Rails will warn about some of these, but the warning is not universal and should not be your control.

Order, pluck, group and friends

Methods taking a SQL fragment are the second cluster. order is the notorious one, because sortable columns are so often driven by a query parameter.

Rails does protect order against raw unrecognised input, but the protection is narrower than people assume and can be worked around with enough determination. The robust answer is not escaping — it is an allowlist. Map the user-supplied sort key to a known column name, and reject anything not in the map. The set of sortable columns is finite and you know it at development time.

The same applies to group, having, joins with a fragment, and select with computed columns.

User input should never be a column name. If it needs to be, it should be a key into a map of column names you wrote.

LIKE, and the wildcards you did not mean

A search with LIKE "%#{query}%" is not SQL injection — the value is still bound — but it has a related bug. % and _ are wildcards inside a LIKE pattern, so a search for 100% matches everything and a search for _ matches every single-character position.

sanitize_sql_like escapes them so the user's input is treated literally. Worth doing for correctness even where there is no security impact, because "search for 100% returns the whole table" is a real bug.

Raw execution

find_by_sql, connection.execute and friends do exactly what you tell them. They are occasionally necessary. When they are, use bind parameters or sanitize_sql_array rather than assembling the string yourself, and treat those call sites as the small audited set they should be.

Making it a review problem, not a memory problem

Two things scale better than remembering.

Static analysis catches the common shapes. Brakeman flags interpolation into SQL fragments with high accuracy, and running it in CI turns this into a build failure rather than a code review someone might skip.

And strong parameters should never be bypassed. Most real incidents I have read about involve a parameter that was permitted for convenience and then flowed somewhere it should not have — the injection was the second step, not the first.

The defaults are good. The job is noticing the handful of places where you have stepped outside them, and being deliberate there.

All writing Reply by email