Table of Contents
- 1. Input Validation: The First Line of Defense
- 2. Output Encoding: Preventing Cross-Site Scripting (XSS)
- 3. Secure Authentication: Protecting User Credentials
- 4. Session Management: Securing User Sessions
- 5. Mitigating Common Attacks
- 6. Secure Dependencies: Keeping Gems Safe
- 7. Error Handling and Logging: Avoiding Information Leaks
- 8. Secure File Handling: Uploads and Storage
- 9. Testing for Security: Catching Vulnerabilities Early
- Conclusion
- References
1. Input Validation: The First Line of Defense
Never trust user input. Whether it’s form data, URL parameters, or API payloads, unvalidated input is the root cause of most security vulnerabilities. Input validation ensures data conforms to expected formats before processing, blocking malicious or malformed inputs.
Best Practices:
- Use Strong Parameters (Rails): Restrict permitted attributes in controllers to prevent mass assignment attacks.
- Validate Data Types and Formats: Check for expected types (e.g., integers, emails) and formats (e.g., phone numbers, zip codes).
- Sanitize Inputs: Remove or escape potentially dangerous characters (e.g.,
<,>,').
Examples:
Strong Parameters in Rails:
# app/controllers/users_controller.rb
class UsersController < ApplicationController
def create
# Only permit :email, :name, and :age; block unexpected attributes like :admin
@user = User.new(user_params)
if @user.save
redirect_to @user, notice: "User created."
else
render :new
end
end
private
def user_params
params.require(:user).permit(:email, :name, :age) # Explicitly allow safe attributes
end
end
Custom Validations with ActiveModel:
# app/models/user.rb
class User < ApplicationRecord
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP } # Validate email format
validates :age, numericality: { only_integer: true, greater_than_or_equal_to: 13 } # Age must be ≥13
validates :name, length: { in: 2..50 } # Name must be 2-50 characters
end
Sanitizing HTML Input (e.g., Comments):
Use the sanitize helper (from rails-html-sanitizer) to strip malicious tags while allowing safe formatting:
# In a view or model
sanitized_comment = ActionView::Base.full_sanitizer.sanitize(params[:comment])
# Allows <b>, <i>, but removes <script>, <iframe>, etc.
2. Output Encoding: Preventing Cross-Site Scripting (XSS)
XSS attacks inject malicious scripts into web pages viewed by other users. Output encoding ensures user-controlled data is rendered as text, not executable code, neutralizing scripts.
Best Practices:
- Leverage Rails’ Auto-Escaping: ERB templates automatically escape variables by default (e.g.,
<%= @user.name %>). - Use
rawSparingly: Only useraw(orhtml_safe) for trusted content; never for user input. - Implement Content Security Policy (CSP): Restrict sources of executable scripts via HTTP headers.
Examples:
Safe Output in ERB:
<!-- app/views/users/show.html.erb -->
<!-- Safe: @user.bio is auto-escaped (e.g., <script> becomes <script>) -->
<p><%= @user.bio %></p>
<!-- Unsafe: raw() skips escaping; only use for trusted, sanitized content -->
<p><%= raw(@user.sanitized_bio) %></p> <!-- Use only if sanitized_bio is pre-validated! -->
Content Security Policy (CSP) in Rails:
Add a CSP header to block unauthorized scripts. Use the secure_headers gem for easy implementation:
# config/initializers/secure_headers.rb
SecureHeaders::Configuration.default do |config|
config.csp = {
default_src: %w['self'], # Only allow scripts from your domain
script_src: %w['self' https://trusted-cdn.com], # Allow scripts from trusted CDNs
style_src: %w['self' 'unsafe-inline' https://trusted-cdn.com], # Inline styles may be needed for some frameworks
img_src: %w['self' data: https://trusted-cdn.com],
object_src: %w['none'], # Block plugins like Flash
upgrade_insecure_requests: true # Force HTTP → HTTPS
}
end
3. Secure Authentication: Protecting User Credentials
Weak authentication is a gateway for attackers. Always hash passwords (never store plaintext) and enforce strong password policies.
Best Practices:
- Use
bcryptfor Password Hashing: Slow hashing algorithms like bcrypt resist brute-force attacks. - Enforce Password Complexity: Require minimum length, mixed case, numbers, and symbols.
- Implement Multi-Factor Authentication (MFA): Add an extra layer with tools like Devise Two-Factor or Roda MFA.
Example: Password Hashing with bcrypt:
- Add
bcryptto your Gemfile:gem 'bcrypt', '~> 3.1.18' - Use
has_secure_passwordin your model (automatically hashes passwords):# app/models/user.rb class User < ApplicationRecord has_secure_password # Adds password hashing and validation validates :password, length: { minimum: 10 }, if: :password_changed? # Enforce password length endhas_secure_passwordstores apassword_digest(hashed) instead of the plaintext password.
4. Session Management: Securing User Sessions
Sessions track authenticated users, making them a prime target. Secure session handling prevents session hijacking or fixation.
Best Practices:
- Use Secure, HttpOnly Cookies: Ensure sessions are stored in cookies with
secure,httponly, andsame_siteflags. - Rotate Session IDs: Invalidate old session IDs after login/logout to prevent fixation.
- Set Expiration: Limit session lifetime to reduce exposure if cookies are stolen.
Example: Configuring Session Cookies in Rails:
# config/initializers/session_store.rb
Rails.application.config.session_store :cookie_store,
key: '_myapp_session', # Unique cookie name
secure: Rails.env.production?, # Only send over HTTPS in production
httponly: true, # Prevent JavaScript access (blocks XSS theft)
same_site: :strict, # Block cross-site requests (mitigates CSRF)
expire_after: 2.hours # Auto-expire after 2 hours of inactivity
5. Mitigating Common Attacks
SQL Injection
SQL injection occurs when untrusted input is embedded directly into SQL queries, allowing attackers to manipulate databases (e.g., delete data, steal credentials).
Best Practices:
- Use Parameterized Queries: Let ActiveRecord handle escaping via
where,find_by, etc. - Avoid Raw SQL: If raw SQL is necessary, use prepared statements or
sanitize_sql.
Examples:
Vulnerable: Direct string interpolation
# UNSAFE: Attacker can inject ' OR '1'='1 to bypass authentication
User.where("email = '#{params[:email]}' AND password = '#{params[:password]}'")
Safe: Parameterized queries
# Safe: ActiveRecord auto-escapes params[:email]
User.where(email: params[:email], password: params[:password])
# Safe: Raw SQL with placeholders (?)
User.where("email = ?", params[:email]) # ? is replaced with escaped value
Cross-Site Request Forgery (CSRF)
CSRF tricks authenticated users into executing unwanted actions (e.g., transferring funds, changing passwords) via malicious links or forms.
Best Practices:
- Use Rails’ Built-In CSRF Protection: Auto-injects authenticity tokens into forms and validates them on submission.
- Validate Tokens for Non-GET Requests: Ensure POST/PUT/DELETE requests include valid tokens.
Example: CSRF Protection in Rails Forms:
<!-- app/views/users/new.html.erb -->
<%= form_with model: @user do |form| %>
<%= form.email_field :email %>
<%= form.submit %>
<!-- Rails auto-injects a hidden authenticity_token field -->
<% end %>
For AJAX requests, include the token in headers:
// app/assets/javascripts/application.js
fetch('/users', {
method: 'POST',
headers: {
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content,
'Content-Type': 'application/json'
},
body: JSON.stringify({ user: { email: '[email protected]' } })
});
Command Injection
Command injection occurs when user input is passed to system commands (e.g., system(), backticks), allowing attackers to execute arbitrary code.
Best Practices:
- Avoid System Commands with User Input: Prefer Ruby libraries over shell commands.
- Use Array Syntax for
system(): Bypass the shell to prevent interpolation of malicious inputs.
Examples:
Vulnerable: Backticks with user input
# UNSAFE: Attacker can inject '; rm -rf /' to delete files
filename = params[:filename]
output = `cat #{filename}` # Executes "cat malicious; rm -rf /" if filename is "malicious; rm -rf /"
Safe: Array syntax (no shell interpolation)
# Safe: Pass arguments as an array to avoid shell parsing
filename = params[:filename]
output = system('cat', filename) # Executes "cat" with filename as a direct argument
6. Secure Dependencies: Keeping Gems Safe
Third-party gems often introduce vulnerabilities. Outdated gems with known flaws (e.g., log4j, heartbleed) are a top attack vector.
Best Practices:
- Audit Gems Regularly: Use
bundler-auditto check for vulnerable gems. - Pin Versions Carefully: Use
~> x.y.zin Gemfile to allow patch updates while avoiding breaking changes. - Update Proactively: Address security advisories promptly (e.g., via Dependabot alerts).
Example: Using bundler-audit:
- Install the gem:
gem install bundler-audit - Run an audit:
Output will flag gems with known issues (e.g., “CVE-2023-1234: rack < 2.2.6.3 - Denial of Service”).bundler-audit check --update # Checks Gemfile.lock against the vulnerability database
7. Error Handling and Logging: Avoiding Information Leaks
Exposing detailed errors (e.g., stack traces) helps attackers identify vulnerabilities. Logging sensitive data (e.g., passwords, tokens) risks data breaches.
Best Practices:
- Hide Errors in Production: Disable detailed exceptions to users.
- Filter Sensitive Data in Logs: Redact passwords, API keys, etc.
- Log Actionable Information: Track security events (e.g., failed logins) without exposing secrets.
Example: Configuring Error Handling in Rails:
# config/environments/production.rb
Rails.application.configure do
config.consider_all_requests_local = false # Hide detailed errors
config.action_dispatch.show_exceptions = true # Show user-friendly error pages
config.exceptions_app = ->(env) { ErrorsController.action(:show).call(env) } # Custom error pages
end
Filtering Sensitive Logs:
# config/initializers/filter_parameter_logging.rb
Rails.application.config.filter_parameters += [:password, :credit_card, :api_token]
# Logs will show "[FILTERED]" instead of the actual value
8. Secure File Handling: Uploads and Storage
File uploads are high-risk: attackers may upload malware, overwrite critical files, or consume disk space.
Best Practices:
- Validate File Types: Check MIME types (not just extensions) and use allowlists.
- Restrict Storage Location: Store uploads outside the web root or use cloud storage (S3, Google Cloud).
- Scan for Malware: Use tools like ClamAV to scan uploads for viruses.
Example: Secure File Uploads with ActiveStorage (Rails):
# app/models/user.rb
class User < ApplicationRecord
has_one_attached :avatar do |attachable|
attachable.variant :thumb, resize: "100x100"
end
validate :validate_avatar
private
def validate_avatar
return unless avatar.attached?
# Validate MIME type (allow only PNG/JPEG)
unless avatar.content_type.in?(%w[image/png image/jpeg])
avatar.purge # Delete invalid file
errors.add(:avatar, "must be a PNG or JPEG.")
end
# Validate file size (< 5MB)
if avatar.byte_size > 5.megabytes
avatar.purge
errors.add(:avatar, "must be less than 5MB")
end
end
end
9. Testing for Security: Catching Vulnerabilities Early
Security testing complements secure coding by identifying gaps before deployment.
Tools and Practices:
- Static Analysis (Brakeman): Scans Rails code for vulnerabilities (XSS, SQLi, CSRF).
- Dynamic Testing (OWASP ZAP): Automated penetration testing to find live vulnerabilities.
- Dependency Scanning (Snyk): Checks for vulnerable gems and container images.
Example: Running Brakeman:
- Install Brakeman:
gem install brakeman - Run in your Rails app:
The report highlights issues like “Unescaped model attribute in view” or “Unsafe use ofbrakeman --format html --output brakeman_report.htmleval”.
Conclusion
Secure coding in Ruby is a continuous process, not a one-time task. By validating inputs, encoding outputs, securing dependencies, and testing rigorously, you can significantly reduce your application’s attack surface. Remember: security is everyone’s responsibility—developers, reviewers, and DevOps teams must collaborate to build resilient systems.
Stay informed about new threats (e.g., via OWASP, RubySec) and update your practices accordingly. With these habits, you’ll protect your users, data, and reputation from harm.