Ruby Rails SQL Injection Vulnerability (Brakeman Check)

Critical Risk SQL Injection
RubyRailsSQL InjectionBrakemanDatabase SecurityActiveRecord

What it is

Application constructs SQL queries using string interpolation or concatenation with user input in Rails, creating SQL injection vulnerabilities that can compromise database security.

class UsersController < ApplicationController
  def search
    # Vulnerable: String interpolation in where clause
    query = params[:query]
    @users = User.where("name LIKE '%#{query}%'")
    
    render json: @users
  end
  
  def find_by_id
    # Vulnerable: Direct interpolation in SQL
    user_id = params[:id]
    @user = User.find_by_sql("SELECT * FROM users WHERE id = #{user_id}")
    
    render json: @user
  end
  
  def update_status
    # Vulnerable: Dynamic SQL with user input
    user_id = params[:user_id]
    status = params[:status]
    
    sql = "UPDATE users SET status = '#{status}' WHERE id = #{user_id}"
    ActiveRecord::Base.connection.execute(sql)
    
    render json: { message: 'Status updated' }
  end
  
  def advanced_search
    # Vulnerable: Complex query building
    conditions = []
    conditions << "name LIKE '%#{params[:name]}%'" if params[:name]
    conditions << "email LIKE '%#{params[:email]}%'" if params[:email]
    
    where_clause = conditions.join(' AND ')
    @users = User.where(where_clause)
    
    render json: @users
  end
end
class UsersController < ApplicationController
  before_action :validate_search_params, only: [:search, :advanced_search]
  
  def validate_search_params
    if params[:query] && params[:query].length > 100
      render json: { error: 'Query too long' }, status: 400
      return
    end
    
    if params[:name] && params[:name].length > 50
      render json: { error: 'Name query too long' }, status: 400
      return
    end
    
    if params[:email] && !params[:email].match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
      render json: { error: 'Invalid email format' }, status: 400
      return
    end
  end
  
  def search
    # Secure: Use parameterized query
    query = params[:query]
    
    if query.present?
      # Safe: ActiveRecord handles parameterization
      @users = User.where('name ILIKE ?', "%#{query}%")
    else
      @users = User.none
    end
    
    render json: @users
  end
  
  def find_by_id
    # Secure: Use ActiveRecord find methods
    user_id = params[:id]
    
    begin
      # Validate ID is numeric
      id = Integer(user_id)
      @user = User.find(id)
      
      render json: @user
    rescue ArgumentError
      render json: { error: 'Invalid user ID format' }, status: 400
    rescue ActiveRecord::RecordNotFound
      render json: { error: 'User not found' }, status: 404
    end
  end
  
  def update_status
    # Secure: Use ActiveRecord update methods
    user_id = params[:user_id]
    status = params[:status]
    
    # Validate status
    allowed_statuses = %w[active inactive pending suspended]
    unless allowed_statuses.include?(status)
      return render json: { error: 'Invalid status' }, status: 400
    end
    
    begin
      id = Integer(user_id)
      user = User.find(id)
      
      # Safe: ActiveRecord handles parameterization
      user.update!(status: status)
      
      render json: { message: 'Status updated', user: user }
    rescue ArgumentError
      render json: { error: 'Invalid user ID format' }, status: 400
    rescue ActiveRecord::RecordNotFound
      render json: { error: 'User not found' }, status: 404
    rescue ActiveRecord::RecordInvalid => e
      render json: { error: e.message }, status: 422
    end
  end
  
  def advanced_search
    # Secure: Build conditions safely
    conditions = {}
    
    if params[:name].present?
      conditions[:name] = params[:name]
    end
    
    if params[:email].present?
      conditions[:email] = params[:email]
    end
    
    if params[:status].present?
      allowed_statuses = %w[active inactive pending suspended]
      if allowed_statuses.include?(params[:status])
        conditions[:status] = params[:status]
      end
    end
    
    # Safe: Use hash conditions
    @users = User.where(conditions)
    
    # Alternative: Use scopes for complex queries
    @users = @users.active if params[:active_only] == 'true'
    @users = @users.recent if params[:recent_only] == 'true'
    
    render json: @users
  end
  
  # Secure alternative using scopes
  def scoped_search
    @users = User.all
    
    # Use safe scopes instead of raw SQL
    @users = @users.with_name(params[:name]) if params[:name].present?
    @users = @users.with_email_domain(params[:domain]) if params[:domain].present?
    @users = @users.created_after(params[:after]) if params[:after].present?
    
    render json: @users
  end
end

# Secure model with scopes
class User < ApplicationRecord
  # Safe scopes that handle parameterization
  scope :with_name, ->(name) { where('name ILIKE ?', "%#{sanitize_sql_like(name)}%") }
  scope :with_email_domain, ->(domain) { where('email ILIKE ?', "%@#{sanitize_sql_like(domain)}") }
  scope :created_after, ->(date) { where('created_at > ?', Date.parse(date)) }
  scope :active, -> { where(status: 'active') }
  scope :recent, -> { where('created_at > ?', 30.days.ago) }
  
  validates :status, inclusion: { in: %w[active inactive pending suspended] }
  validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }
end

💡 Why This Fix Works

See fix suggestions for detailed explanation.

Why it happens

Rails query with interpolation: User.where("email = '#{params[:email]}'"). String interpolation doesn't parameterize. SQL injection through params[:email]. Quote escaping vulnerable. ActiveRecord where() requires parameterized queries for security. String interpolation creates injection vectors.

Root causes

Using String Interpolation in ActiveRecord where() Clauses

Rails query with interpolation: User.where("email = '#{params[:email]}'"). String interpolation doesn't parameterize. SQL injection through params[:email]. Quote escaping vulnerable. ActiveRecord where() requires parameterized queries for security. String interpolation creates injection vectors.

Using Raw SQL with User Input in find_by_sql

Raw SQL queries: User.find_by_sql("SELECT * FROM users WHERE name = '#{name}'"). find_by_sql executes raw SQL. String formatting bypasses ORM protection. User input in SQL strings. All raw SQL needs parameterization. ActiveRecord provides safe alternatives.

Concatenating User Input in SQL Conditions

String concatenation: condition = 'username = "' + params[:user] + '"'; User.where(condition). + operator builds SQL strings. No parameterization. User controls query logic. Should use: User.where(username: params[:user]). Hash syntax or placeholders required.

Using String Conditions Without Parameterization

Unparameterized conditions: User.where('role = ?', params[:role]) safe but User.where('role = ' + params[:role]) vulnerable. Question mark placeholders required. Array syntax for parameters. String building without placeholders creates SQL injection.

Building Dynamic ORDER BY or LIMIT Clauses

Dynamic sorting: User.order("#{params[:sort]} #{params[:direction]}"). ORDER BY clauses with user input. Column and direction from params. Cannot use parameters for identifiers. Requires allowlist validation. String interpolation in order() vulnerable.

Fixes

1

Use Hash Syntax for Simple WHERE Conditions

Safe hash conditions: User.where(email: params[:email]) or User.where(role: role, active: true). ActiveRecord handles parameterization. Hash syntax preferred. Type-safe. No SQL injection risk. Simple and readable. Use for equality checks.

2

Use Array Syntax with Placeholders for Complex Conditions

Parameterized queries: User.where(['email = ? AND active = ?', params[:email], true]) or User.where('age > :min_age', min_age: params[:age]). Question mark or named placeholders. Array passes parameters. ActiveRecord escapes values. Safe for all conditions.

3

Use ActiveRecord Query Interface Instead of Raw SQL

ORM methods: User.joins(:posts).where(posts: {published: true}).order(created_at: :desc). Chainable query interface. Automatic parameterization. Type safety. Database abstraction. Avoid find_by_sql when possible. ORM methods prevent SQL injection.

4

Validate Dynamic Identifiers with Allowlists

Allowlist for ORDER BY: ALLOWED_SORT = ['name', 'email', 'created_at']; ALLOWED_DIR = ['asc', 'desc']; raise unless ALLOWED_SORT.include?(params[:sort]) && ALLOWED_DIR.include?(params[:dir]); User.order("#{params[:sort]} #{params[:dir]}"). Validate identifiers. Can't parameterize column names. Allowlist essential.

5

Use Arel for Complex Dynamic Queries

Arel query builder: users = User.arel_table; query = users[:email].eq(params[:email]).and(users[:active].eq(true)); User.where(query). Programmatic query construction. Type-safe. Prevents injection through structure. Advanced query building without raw SQL.

6

Run Brakeman to Detect SQL Injection Vulnerabilities

Use Brakeman scanner: brakeman -A detects SQL injection. Review SQL Injection warnings. Fix flagged queries. Automated detection in CI/CD. Regular scans prevent unsafe queries. Brakeman specifically checks ActiveRecord with user input.

Detect This Vulnerability in Your Code

Sourcery automatically identifies ruby rails sql injection vulnerability (brakeman check) and many other security issues in your codebase.