cyberangles guide

Secure Coding Practices in Ruby: Protecting Your Applications

Ruby, with its elegant syntax and robust ecosystem (most notably Ruby on Rails), is a popular choice for building web applications. However, like any programming language, Ruby applications are vulnerable to security threats if not developed with care. From SQL injection to cross-site scripting (XSS), attackers exploit common vulnerabilities to compromise data, disrupt services, or gain unauthorized access. Secure coding isn’t just about reacting to threats—it’s a proactive approach to building resilient applications. In this blog, we’ll explore critical secure coding practices tailored to Ruby and Rails, with actionable examples to help you protect your applications from day one. Whether you’re building a small Sinatra app or a large Rails platform, these practices will fortify your code against common and emerging threats.

Table of Contents

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 raw Sparingly: Only use raw (or html_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 &lt;script&gt;) -->
<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 bcrypt for 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:

  1. Add bcrypt to your Gemfile:
    gem 'bcrypt', '~> 3.1.18'
  2. Use has_secure_password in 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
    end
    has_secure_password stores a password_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, and same_site flags.
  • 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-audit to check for vulnerable gems.
  • Pin Versions Carefully: Use ~> x.y.z in Gemfile to allow patch updates while avoiding breaking changes.
  • Update Proactively: Address security advisories promptly (e.g., via Dependabot alerts).

Example: Using bundler-audit:

  1. Install the gem:
    gem install bundler-audit
  2. Run an audit:
    bundler-audit check --update # Checks Gemfile.lock against the vulnerability database
    Output will flag gems with known issues (e.g., “CVE-2023-1234: rack < 2.2.6.3 - Denial of Service”).

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:

  1. Install Brakeman:
    gem install brakeman
  2. Run in your Rails app:
    brakeman --format html --output brakeman_report.html
    The report highlights issues like “Unescaped model attribute in view” or “Unsafe use of eval”.

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.

References