RSTR-INJ-008 — Rails .where("... #{params[...]} ...")
Summary
A Rails ActiveRecord query method (.where, .find_by_sql,
.update_all, .delete_all, etc.) is called with a string-interpolated
SQL fragment that contains params[...]. Ruby evaluates the #{...}
substitution before the string reaches ActiveRecord, so the database
sees an attacker-controlled query — classic SQL injection.
ActiveRecord's hash form (.where(id: params[:id])) and the positional
form (.where("id = ?", params[:id])) both delegate to prepared
statements and are safe.
Severity
Critical.
Languages
Ruby (Rails).
What rastray flags
User.where("id = '#{params[:user][:id]}'") # ← flagged
User.find_by_sql("SELECT * FROM users WHERE id = #{params[:id]}") # ← flagged
Order.update_all("status = '#{params[:s]}'") # ← flagged
What rastray deliberately does not flag
Safe ActiveRecord forms:
User.where(id: params[:id]) # hash form
User.where("id = ?", params[:id]) # positional
User.where("id = :id", id: params[:id]) # named
These compile to prepared statements; the value never reaches the SQL parser as code.
How to fix it
Use one of the two parameterised forms:
# Hash form (preferred when matching one column)
User.where(id: params[:id])
# Positional form (when you need a custom predicate)
User.where("name LIKE ?", "%#{params[:q]}%")
# Named placeholders for multi-column queries
User.where("name LIKE :q OR email LIKE :q", q: "%#{params[:q]}%")
For more complex queries, build the query progressively:
scope = User.all
scope = scope.where(role: params[:role]) if params[:role].present?
scope = scope.where("created_at >= ?", since) if since.present?
scope.order(:name)
Even when the value looks "safe" (a number, a UUID), use the parameterised form. Type coercion happens at the binding layer and removes any ambiguity.