mcpField
S
security

security

github.com/thinkoodleSecurity7178 installsUpdated 7w ago
Repository

About security

Rails Security Expert is a security guidance skill for Rails development, covering practices from input handling to sessions, secrets, and logging. It is intended for developers implementing or reviewing Rails features, with an emphasis on retaining built-in protections and limiting sensitive-data exposure.

Key features

  1. 01Guide CSRF and XSS protection
  2. 02Use parameterized queries and validate input
  3. 03Restrict writable fields with strong parameters
  4. 04Provide authentication and authorization guidance
  5. 05Cover sessions, secrets, CSP, and secure logging

Use cases

  1. 01Build controllers or APIs that accept user input
  2. 02Handle user-generated content and file uploads
  3. 03Configure authentication, sessions, and access control
  4. 04Review or fix Rails security issues

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/thinkoodle/rails-skills --skill security

After installing, type/securityin Claude Code to confirm it loaded.

Bundled files

References and scripts installed along with SKILL.md.

references/
advanced-threats.md
authentication.md
credentials.md
csp-headers.md
csrf.md
file-uploads.md
scanning-tools.md
sessions.md
sql-injection.md
xss.md

Details

From the project README

Rails Security Expert

Write secure Rails code by default. Security mistakes are the most dangerous mistakes an agent can make — an XSS hole or SQL injection can compromise every user instantly. When in doubt, choose the safer path.

Philosophy

Core Principles:

  1. Secure by default — Rails has excellent built-in protections. Never disable them without explicit, documented justification
  2. Defense in depth — Layer protections. Don't rely on a single mechanism
  3. Permit-list over deny-list — Always prefer allowed lists over blocked lists for input validation, URL schemes, HTML tags, redirect targets
  4. Never trust user input — Every param, header, cookie, and URL segment is attacker-controlled until proven otherwise
  5. Fail closed — When authorization is ambiguous, deny access. When input is suspicious, reject it
  6. Minimize exposure — Log less, expose less, store less. Filter sensitive params, avoid leaking stack traces, encrypt at rest

When To Use This Skill

  • Adding authentication or authorization logic
  • Writing controllers that accept user input
  • Rendering user-generated content in views
  • Building API endpoints
  • Working with file uploads or downloads
  • Configuring sessions, cookies, or CSRF protection
  • Managing secrets and credentials
  • Setting HTTP security headers or CSP
  • Reviewing code for security vulnerabilities
  • Fixing reported CVEs or security issues

Instructions

Step 1: Audit the Existing Security Posture

Check what's already in place before changing anything — understanding the existing security posture prevents accidentally weakening it:

# Check CSRF configuration
rg "protect_from_forgery" app/controllers/

# Check strong params usage
rg "params\.permit\|params\.require" app/controllers/

# Check for dangerous patterns
rg "\.html_safe|raw\(|raw " app/views/ app/helpers/
rg "\.where\(\".*#\{" app/ --type ruby
rg "redirect_to.*params" app/controllers/

# Check security headers
rg "content_security_policy" config/
rg "force_ssl" config/

# Check filter_parameters
cat config/initializers/filter_parameter_logging.rb

# Check credentials setup
ls config/credentials* config/master.key 2>/dev/null
Step 2: CSRF Protection

Never disable CSRF protection. Rails enables it by default — it's your primary defense against cross-site request forgery.

# ApplicationController — this should ALREADY be here
class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
end

How it works:

  • Every non-GET request requires an authenticity token
  • Include <%= csrf_meta_tags %> in your layout <head> for Turbo/Ajax
  • API controllers using token auth may skip CSRF — but only if they don't use cookie-based sessions (cookie sessions + no CSRF = attackers can forge requests)
  • Never use skip_forgery_protection on controllers that serve browser sessions
# API-only controllers with token auth — acceptable to skip
class Api::BaseController < ActionController::API
  # No cookie session = no CSRF needed
  # But MUST authenticate via Authorization header
end

# WRONG — disabling CSRF on a browser-facing controller
class PaymentsController < ApplicationController
  skip_forgery_protection  # NEVER DO THIS
end
Step 3: XSS Prevention

Rails auto-escapes all ERB output by default. This is your primary XSS defense — bypassing it means any user-controlled string can inject scripts into other users' browsers.

# SAFE — auto-escaped (default)
<%= user.name %>
<%= user.bio %>

# DANGEROUS — bypasses escaping. Only use for trusted, sanitized HTML
<%= raw(content) %>              # AVOID
<%= content.html_safe %>         # AVOID
<%== content %>                  # AVOID

# CORRECT way to render trusted HTML — sanitize first
<%= sanitize(user.bio, tags: %w[p br strong em a ul ol li], attributes: %w[href title]) %>

html_safe discipline:

  • NEVER call .html_safe on user input — this is an instant XSS hole
  • NEVER call .html_safe on strings built with interpolation of user data — the interpolated values bypass escaping
  • Only use .html_safe on string literals you fully control
  • When building HTML in helpers, use tag builder or content_tag — they handle escaping automatically
# WRONG — XSS vulnerability
def greeting(user)
  "<h1>Hello, #{user.name}</h1>".html_safe
end

# CORRECT — use tag helpers
def greeting(user)
  tag.h1("Hello, #{user.name}")
end

# CORRECT — escape manually if you must build strings
def greeting(user)
  "<h1>Hello, #{ERB::Util.html_escape(user.name)}</h1>".html_safe
end

sanitize() usage:

  • Specify allowed tags and attributes explicitly — the default allowlist may be broader than you need
  • For rich text, prefer Action Text which handles sanitization automatically
Step 4: SQL Injection Prevention

Never interpolate user input into SQL strings. Interpolation lets attackers inject arbitrary SQL — they can read, modify, or delete any data in your database.

# DANGEROUS — SQL injection
User.where("name = '#{params[:name]}'")
User.where("email LIKE '%#{params[:q]}%'")
Order.where("status = '#{status}' AND user_id = #{user_id}")

# SAFE — parameterized queries
User.where("name = ?", params[:name])
User.where("email LIKE ?", "%#{User.sanitize_sql_like(params[:q])}%")
Order.where(status: status, user_id: user_id)

# SAFE — named parameters
User.where("name = :name AND role = :role", name: params[:name], role: params[:role])

# SAFE — hash conditions (preferred)
User.where(name: params[:name])

LIKE queries need special handling:

# WRONG — user can inject % and _ wildcards
User.where("name LIKE ?", "%#{params[:q]}%")

# CORRECT — sanitize LIKE wildcards
User.where("name LIKE ?", "%#{User.sanitize_sql_like(params[:q])}%")

Less obvious injection vectors — these methods also pass strings directly to SQL:

  • order() — never pass raw user input (attackers can inject subqueries)
  • pluck(), select(), group(), having(), joins() with string args
  • find_by_sql(), connection.execute()
  • Arel.sql() — only use with trusted strings, never user input
# WRONG — order injection
Post.order(params[:sort])

# CORRECT — permit-list of allowed columns
allowed = %w[created_at title updated_at]
direction = %w[asc desc].include?(params[:dir]) ? params[:dir] : "asc"
column = allowed.include?(params[:sort]) ? params[:sort] : "created_at"
Post.order("#{column} #{direction}")
Step 5: Mass Assignment Protection

Use strong parameters for every model write. Raw params let attackers set any attribute — including admin flags, roles, and associations they shouldn't touch.

# WRONG — mass assignment vulnerability
User.create(params[:user])
@user.update(params[:user])
User.new(params.permit!)  # permit! allows everything

# CORRECT — strong parameters
def user_params
  params.require(:user).permit(:name, :email, :bio)
end

@user.update(user_params)

Strong params guidelines:

  • Define a private *_params method in every controller
  • Only permit attributes the user should be able to set
  • NEVER permit :role, :admin, :is_admin, :verified or similar privilege attributes from user input — this is how privilege escalation happens
  • Nested attributes need explicit permitting
def post_params
  params.require(:post).permit(
    :title, :body, :published,
    tags: [],
    images_attributes: [:id, :url, :caption, :_destroy]
  )
end
Step 6: Authentication

Passwordless email-code auth (Basecamp, Fizzy, Herald): do not use the Rails 8 password generator. Follow the magic-link-auth skill — codes for humans, session/pending/access tokens for machines.

Password-based apps: start from the Rails 8+ authentication generator:

bin/rails generate authentication

Key authentication practices:

  • Use has_secure_password — it handles bcrypt hashing correctly
  • Use authenticate_by — it's timing-safe, preventing attackers from enumerating valid accounts by measuring response time
  • Call reset_session after login to prevent session fixation (where an attacker pre-sets a session ID)
  • Require current password for password/email changes
  • Use generic error messages: "Invalid email or password" (not "User not found") — specific messages reveal which accounts exist
# Session creation — correct pattern
class SessionsController < ApplicationController
  def create
    if user = User.authenticate_by(
      email_address: params[:email_address],
      password: params[:password]
    )
      reset_session  # Prevent session fixation
      start_new_session_for user
      redirect_to root_path
    else
      redirect_to new_session_path, alert: "Invalid email or password."
    end
  end
end

Rate limiting login attempts:

class SessionsController < ApplicationController
  rate_limit to: 10, within: 3.minutes, only: :create
end
Step 7: Authorization

Scope queries to the current user. Without scoping, any authenticated user can access any record just by changing the ID in the URL.

# WRONG — any user can access any project by changing the ID
@project = Project.find(params[:id])

# CORRECT — scope to current user
@project = Current.user.projects.find(params[:id])

# CORRECT — use policy objects (Pundit, Action Policy, etc.)
@project = authorize Project.find(params[:id])

Authorization checklist:

  • Scope every query to the authenticated user's permissions
  • Check authorization on every action (before_action)
  • Use before_action :authenticate for all protected resources
  • Prefer deny-by-default: before_action in ApplicationController with selective skip_before_action is safer than opt-in only: (new actions are protected by default instead of accidentally exposed)
class ApplicationController < ActionController::Base
  before_action :require_authentication
  # All controllers require auth unless explicitly skipped
end

class PublicPagesController < ApplicationController
  skip_before_action :require_authentication, only: [:home, :about]
end
Step 8: Session Security
# config/environments/production.rb
config.force_ssl = true  # HSTS + secure cookies + HTTPS redirect

# Session cookie settings (Rails defaults are good, but verify)
Rails.application.config.session_store :cookie_store,
  key: "_myapp_session",
  secure: Rails.env.production?,
  httponly: true,
  same_site: :lax

Session guidelines:

  • Store only IDs in the session — sensitive data in cookies is readable even when signed
  • Call reset_session after login (prevents session fixation)
  • Implement session expiry (absolute + idle timeout)
  • force_ssl = true in production — without it, session cookies travel over plain HTTP and can be stolen on public networks
Step 9: Secrets and Credentials

Use Rails credentials or environment variables for secrets. Hardcoded secrets end up in version control, where anyone with repo access (or a leaked backup) gets your API keys.

# Edit credentials
EDITOR=vim bin/rails credentials:edit

# Environment-specific credentials
EDITOR=vim bin/rails credentials:edit --environment production
# Access credentials
Rails.application.credentials.secret_key_base
Rails.application.credentials.dig(:aws, :access_key_id)

# Use bang version to fail loud if missing
Rails.application.credentials.some_api_key!

Key management:

  • NEVER commit config/master.key or config/credentials/*.key — these decrypt all your secrets
  • Add them to .gitignore (Rails does this by default, but verify)
  • Use ENV["RAILS_MASTER_KEY"] in production
  • Avoid logging or exposing credentials in error messages
  • Rotate secrets immediately if they may have been exposed
Step 10: Content Security Policy
# config/initializers/content_security_policy.rb
Rails.application.config.content_security_policy do |policy|
  policy.default_src :self, :https
  policy.font_src    :self, :https, :data
  policy.img_src     :self, :https, :data
  policy.object_src  :none
  policy.script_src  :self, :https
  policy.style_src   :self, :https
  policy.connect_src :self, :https
  policy.frame_ancestors :none
  policy.report_uri "/csp-violation-report-endpoint"
end

# Use nonces instead of 'unsafe-inline'
Rails.application.config.content_security_policy_nonce_generator =
  ->(request) { SecureRandom.base64(16) }
Rails.application.config.content_security_policy_nonce_directives = %w[script-src]

Layout must include:

<head>
  <%= csrf_meta_tags %>
  <%= csp_meta_tag %>
  <%= javascript_include_tag "application", nonce: true %>
  <%= stylesheet_link_tag "application", nonce: true %>
</head>
Step 11: Logging Security
# config/initializers/filter_parameter_logging.rb
Rails.application.config.filter_parameters += [
  :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp,
  :ssn, :credit_card, :card_number, :cvv, :authorization
]

Avoid logging (these show up in log files, error trackers, and potentially third-party services):

  • Passwords, tokens, API keys, secrets
  • Credit card numbers, SSNs, PII
  • Full request bodies that may contain sensitive data
Step 12: Redirect Security
# WRONG — open redirect vulnerability
redirect_to params[:return_to]

# CORRECT — only allow internal paths
redirect_to safe_redirect_path(params[:return_to])

# Helper implementation
def safe_redirect_path(path)
  uri = URI.parse(path.to_s)
  if uri.relative? && uri.path.start_with?("/")
    uri.path
  else
    root_path
  end
rescue URI::InvalidURIError
  root_path
end

# SIMPLEST — use url_from (Rails 7+)
redirect_to url_from(params[:return_to]) || root_path
Step 13: File Upload Security
# Validate content type
class Document < ApplicationRecord
  has_one_attached :file

  validates :file, content_type: %w[application/pdf image/png image/jpeg],
                   size: { less_than: 10.megabytes }
end

# NEVER serve uploads from your app domain in production
# Use a separate domain/CDN or Active Storage's redirect mode

# NEVER use user-provided filenames for storage
# Active Storage handles this — it uses random keys

File upload guidelines:

  • Validate content type AND file extension — attackers can spoof one but rarely both
  • Set maximum file size limits to prevent denial-of-service via large uploads
  • Store uploads outside the web root (Active Storage does this automatically)
  • Never execute uploaded files
  • Process images/media asynchronously — image processing libraries can have vulnerabilities
  • Scan for malware in sensitive applications

Quick Reference

Dangerous Patterns to Grep For
# XSS: rg "\.html_safe|raw\(|<%==" app/
# SQL injection: rg '\.where\(".*#\{' app/ --type ruby
# Order injection: rg '\.order\(params' app/ --type ruby
# Mass assignment: rg 'params\.permit!' app/controllers/
# Open redirects: rg 'redirect_to.*params' app/controllers/
# CSRF disabled: rg 'skip_forgery_protection' app/controllers/
# Hardcoded secrets: rg '(api_key|secret|password|token)\s*=' app/ config/ -i
# Command injection: rg 'system\(|exec\(|`.*#\{' app/ --type ruby
Regex Gotcha
# WRONG — ^ and $ match LINE boundaries in Ruby, not string boundaries
validates :url, format: { with: /^https?:\/\/.+$/ }

# CORRECT — use \A and \z for string boundaries
validates :url, format: { with: /\Ahttps?:\/\/.+\z/ }

Anti-Patterns — Each of These Is a Vulnerability

  1. html_safe on user input — Instant XSS. Attackers inject scripts that run in other users' browsers
  2. String interpolation in SQL — SQL injection lets attackers read/modify/delete your entire database
  3. params.permit! — Permits everything, letting attackers set admin flags, roles, or any attribute
  4. skip_forgery_protection on browser controllers — Lets malicious sites submit forms on behalf of your users
  5. redirect_to params[:url] — Open redirect. Attackers craft links that look like your site but redirect to phishing pages
  6. Hardcoded secrets in source — Anyone with repo access (or a leaked backup) gets your API keys
  7. Kernel#open with user input — open("| rm -rf /") executes shell commands. Use File.open, URI.open, or IO.open
  8. Disabling force_ssl in production — Session cookies travel in plaintext, stealable on any shared network
  9. Logging sensitive data — Logs end up in error trackers, log aggregators, and developer machines
  10. Trusting client-side validation — Attackers bypass JavaScript trivially; server-side validation is the real gate

Security Audit Checklist

Before shipping any feature, verify:

  • CSRF protection enabled (not skipped)
  • Strong parameters used in every controller action
  • No .html_safe or raw() on user-supplied content
  • All SQL uses parameterized queries
  • Queries scoped to current user (authorization)
  • Redirects validated (no open redirects)
  • Sensitive params filtered from logs
  • File uploads validated (type, size)
  • No hardcoded secrets in source
  • force_ssl = true in production
  • CSP headers configured
  • Authentication uses timing-safe comparison
  • Session reset after login
  • Rate limiting on login/sensitive endpoints

Comments

Markdown supported