flawopen.com/SQL Injection/Ruby

SQL Injection in Ruby

Critical CWE-89 Draft — pending review
ELI5

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.

Key terms on this page
user-controlled input
Any value that ultimately came from whoever is using — or attacking — the app: a form field, a URL parameter, an uploaded filename, an HTTP header. The app can't assume it's well-formed or safe.
SQL query
The command sent to a database — e.g. "get this row," "delete this table." Its meaning comes entirely from its exact text, which is what makes injecting extra text into it dangerous.

What's happening

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.

Real-world impact

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.

Vulnerable vs. fixed

VULNERABLE
# params[:id] straight into the string
User.where(
  "id = #{params[:id]}"
)
FIXED
# value bound, never interpolated
User.where(
  "id = ?", params[:id]
)
# or, more idiomatic still:
User.where(id: params[:id])

Why the fix works

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.

Ruby/Rails-specific gotchas

String interpolation inside where() is the single most common Rails SQLi pattern

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.

find_by_sql and raw connection calls bypass ActiveRecord entirely

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.

Ordering and column-name inputs need a different defense

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.

Common misconceptions

"ActiveRecord always protects me"

True for its hash and bound-parameter query forms — false for raw string conditions, find_by_sql, and direct connection calls.

"This value came from a Rails form helper, so it's sanitized"

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.

How to check if you're affected

grep -rn 'where("' app/ | grep '#{' grep -rn "find_by_sql\|connection.execute" app/
Brakeman, the Rails-specific static analyzer, flags SQL injection (warning type "SQL Injection") automatically — run it in CI.

Prevention checklist

FAQ

Is the hash form always available?

Only for straightforward equality conditions. Anything involving operators, ranges, or raw SQL fragments needs the ? bound-parameter form instead.

What about sorting by a user-chosen column?

Parameter binding doesn't apply to identifiers like column names — validate against an explicit allow-list of permitted columns before passing anything to order().

References

View in: Python JavaScript Go Java PHP C# Ruby C/C++ Rust Kotlin Swift Solidity (N/A)
Also see: Command InjectionPath Traversal XSSInsecure Deserialization