flawopen.com/SQL Injection/Ruby
Imagine a form that only ever expects a ticket number, like 482. SQL injection is what happens when someone types something sneaky into that box instead of a number — a trick phrase that makes the system say "show me every ticket" instead of just ticket 482, because the system never checked that what it received was actually just a number.
SQL injection happens when user-controlled input gets inserted directly into a database query's text. In Rails specifically, this almost always means a raw string condition passed to where() with interpolation — a pattern that still "looks like" idiomatic ActiveRecord usage, which is exactly what makes it easy to miss.
In 2015, UK telecom TalkTalk suffered a breach affecting over 150,000 customers after attackers exploited a SQL injection flaw in a legacy web page inherited through a company acquisition. The UK's data protection regulator fined TalkTalk £400,000, describing the failure as preventable and basic.
Source: UK Information Commissioner's Office enforcement notice, 2016 — see References below.# params[:id] straight into the string
User.where(
"id = #{params[:id]}"
)
# value bound, never interpolated User.where( "id = ?", params[:id] ) # or, more idiomatic still: User.where(id: params[:id])
The ? form tells ActiveRecord to bind the value as a separate query parameter rather than splicing it into the condition string — the database driver attaches it after the query's structure is fixed. The hash form where(id: ...) goes further and never builds a raw SQL fragment at all, which is why it's the preferred idiom whenever the condition is a simple equality check.
where("id = #{x}") compiles and runs exactly like normal ActiveRecord code, so it doesn't stand out in review the way an obviously raw query would — the danger is entirely in what's inside the interpolation.
ActiveRecord::Base.connection.execute(sql) and Model.find_by_sql(sql) run exactly what you give them, with no automatic parameter binding — any interpolation here is as dangerous as it would be in raw JDBC or PDO.
order(params[:sort]) can't be fixed with ? binding, because column/direction names aren't values — they need an explicit allow-list check before being used, not parameterization.
True for its hash and bound-parameter query forms — false for raw string conditions, find_by_sql, and direct connection calls.
Form helpers control HTML rendering, not what an attacker can actually submit in the underlying HTTP request — they provide no SQL-layer protection at all.
grep -rn 'where("' app/ | grep '#{'
grep -rn "find_by_sql\|connection.execute" app/
Only for straightforward equality conditions. Anything involving operators, ranges, or raw SQL fragments needs the ? bound-parameter form instead.
Parameter binding doesn't apply to identifiers like column names — validate against an explicit allow-list of permitted columns before passing anything to order().