7 Essential Rails Security Techniques Every Developer Must Know in 2024
Learn how to build secure Ruby on Rails applications with proven security techniques. Protect against SQL injection, XSS, CSRF attacks, and more with practical code examples.
Building secure web applications demands constant attention. I’ve spent years working with Ruby on Rails, and security remains a top priority. Modern threats evolve rapidly, so layered defenses are essential. Let’s explore practical techniques that significantly enhance application safety.
SQL injection attacks manipulate database queries through malicious input. Rails protects us through ActiveRecord’s parameterized queries. Never interpolate user input directly into SQL strings. Instead, use placeholders that separate data from instructions. Here’s how I implement this:
# UNSAFE: Vulnerable to injection
User.where("name = '#{params[:name]}'")
# SAFE: Parameterized query
User.where("name = ?", params[:name])
# Also safe with hash condition
User.where(name: params[:name])
ActiveRecord escapes parameters automatically, neutralizing injection attempts. For complex queries, I use Arel or sanitize_sql helpers. Remember: raw SQL fragments require manual sanitization. I always double-check these cases during code reviews.
Cross-site scripting (XSS) attacks inject malicious scripts into web pages. Rails combats this through automatic HTML escaping in views. Use <%= %> tags for escaped output and <%- raw %> only when necessary. For rich text content, I implement strict sanitization:
# In controller
def sanitize_content
ActionController::Base.helpers.sanitize(params[:html_content],
tags: %w[p b i ul li],
attributes: %w[href style]
)
end
# In view (ERB)
<%= @user_input %> <!-- Auto-escaped -->
<%= raw sanitize(@rich_content) %> <!-- Sanitized before rendering -->
I configure Content Security Policy (CSP) headers as additional protection. This restricts sources for scripts, styles, and other resources. Add this to config/initializers/content_security_policy.rb:
Rails.application.config.content_security_policy do |policy|
policy.default_src :self
policy.script_src :self, :https
policy.style_src :self, :https
policy.img_src :self, :https, :data
policy.report_uri "/csp_reports"
end
Cross-site request forgery (CSRF) tricks users into executing unwanted actions. Rails includes built-in protection via authenticity tokens. Ensure this exists in your application layout:
<%= csrf_meta_tags %>
And in controllers, keep this default protection:
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
end
For API endpoints, I disable CSRF protection and implement token-based authentication instead. Always verify the HTTP referer for sensitive operations as an extra precaution.
Mass assignment vulnerabilities allow attackers to modify protected attributes. Strong Parameters enforce allowlisting. I always define explicit permit lists:
def user_params
params.require(:user).permit(:name, :email, :password)
end
@user = User.new(user_params)
Never use params.permit! - this disables protection entirely. For nested parameters, specify exactly which attributes are allowed:
params.require(:project).permit(:title, tasks_attributes: [:id, :description, :_destroy])
Password security requires multiple layers. I enforce complexity requirements during validation:
class User < ApplicationRecord
validates :password, length: { minimum: 12 },
format: { with: /\A(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/ }
end
Store passwords using bcrypt hashing, which Rails handles automatically through has_secure_password. Implement account lockouts after repeated failed attempts:
def authenticate
user = User.find_by(email: params[:email])
if user&.authenticate(params[:password])
# Successful login
else
user.increment!(:failed_attempts)
lock_account if user.failed_attempts > 5
end
end
Session security prevents hijacking attacks. Configure cookies with secure attributes in config/initializers/session_store.rb:
Rails.application.config.session_store :cookie_store,
key: '_my_app_session',
same_site: :lax,
secure: Rails.env.production?,
httponly: true
Rotate session tokens after login and logout. I implement expiration for sensitive sessions:
def create_session
session[:user_id] = @user.id
session[:expires_at] = 30.minutes.from_now
end
def check_session
redirect_to login_path if session[:expires_at] < Time.current
end
Authorization controls access to resources. I use policy objects to encapsulate permission logic:
class ProjectPolicy
attr_reader :user, :project
def initialize(user, project)
@user = user
@project = project
end
def edit?
user.admin? || project.owner == user
end
end
# In controller
def edit
@project = Project.find(params[:id])
authorize_project
end
private
def authorize_project
redirect_to root_path, alert: "Access denied" unless ProjectPolicy.new(current_user, @project).edit?
end
Always scope database queries to current user resources:
def show
@document = current_user.documents.find(params[:id])
end
Dependency management prevents known vulnerabilities. I integrate bundler-audit and brakeman into CI pipelines:
# Regularly scan gems
bundle audit check --update
# Run Brakeman security scanner
brakeman -q -w1
Set up automated security notifications for gem vulnerabilities. In Gemfile, pin critical dependencies to specific versions:
gem 'rails', '~> 7.0.4.3'
gem 'devise', '>= 4.9.0'
Audit logging provides crucial forensic data. I implement detailed activity tracking:
class AuditLog
def self.record(event_type, user, details)
log_entry = {
timestamp: Time.current,
event: event_type,
user_id: user.id,
ip: user.current_sign_in_ip,
details: details
}
Rails.logger.info(log_entry.to_json)
end
end
# Usage in controller
AuditLog.record(:password_change, current_user, {method: "web_ui"})
Regular patching maintains security posture. I schedule monthly security reviews and apply framework updates promptly. For critical vulnerabilities, apply patches immediately after testing. Maintain a vulnerability response checklist that includes:
- Impact assessment
- Patch verification
- Deployment scheduling
- Communication plan
Security requires continuous effort. I integrate these practices throughout the development lifecycle. Automated tests should verify security controls:
test "should sanitize script tags in content" do
post :create, params: { content: '<script>alert()</script>' }
assert_no_match '<script>', response.body
end
test "admin required for user deletion" do
sign_in users(:regular_user)
delete :destroy, params: { id: users(:another_user).id }
assert_response :forbidden
end
These techniques form a comprehensive security approach. Implement them consistently to protect your applications effectively. Remember to adapt as new threats emerge.