CrackedRuby CrackedRuby

Overview

Security by Design represents a proactive approach to software security where security considerations drive architectural decisions, implementation patterns, and operational procedures from project inception. This methodology contrasts with reactive security models that attempt to secure systems after development completes.

The concept emerged from recurring patterns of security failures in systems where developers treated security as a feature to add later. When security integrates into the foundation of system design, the resulting software resists attacks more effectively and requires fewer expensive retrofits. Security by Design applies to all layers of application development: architecture, data models, authentication systems, authorization logic, input handling, error management, and deployment configurations.

A system built with Security by Design considers threat models during architecture planning, validates inputs at system boundaries, enforces the principle of least privilege in authorization, and fails securely when errors occur. The approach recognizes that perfect security remains impossible but that intentional design choices significantly reduce attack surfaces and limit the impact of successful attacks.

# Security by Design: Input validation at system boundary
class UserRegistration
  def initialize(params)
    @email = sanitize_email(params[:email])
    @password = params[:password]
  end

  def register
    validate_email_format
    validate_password_strength
    check_email_uniqueness
    
    User.create!(
      email: @email,
      password_digest: BCrypt::Password.create(@password)
    )
  end

  private

  def sanitize_email(email)
    email.to_s.strip.downcase
  end

  def validate_email_format
    raise ValidationError unless @email.match?(/\A[^@\s]+@[^@\s]+\.[^@\s]+\z/)
  end

  def validate_password_strength
    raise ValidationError if @password.length < 12
    raise ValidationError unless @password.match?(/[A-Z]/) && @password.match?(/[0-9]/)
  end
end

Key Principles

Principle of Least Privilege dictates that each component, user, and process receives only the minimum permissions necessary to perform its function. A database connection pool accessing user data requires read access to specific tables, not administrative privileges across the entire database. An API endpoint serving public product information operates under different authorization rules than an endpoint modifying inventory records.

Defense in Depth structures security as multiple independent layers. If an attacker bypasses one security control, additional controls prevent complete system compromise. Web applications implement defense in depth through input validation, parameterized queries, output encoding, content security policies, and rate limiting. Each layer provides protection, and no single layer failure compromises the entire system.

Fail Securely ensures that error conditions default to secure states rather than permissive states. When authentication fails, the system denies access rather than logging in a default user. When authorization checks encounter errors, the system refuses the operation rather than proceeding with uncertain permissions. Database connection failures prevent data access rather than bypassing access controls.

Complete Mediation requires the system to check authorization on every access to protected resources. Session-based web applications verify user permissions on each request, not just at login. API endpoints validate authentication tokens for every call. Database queries include user context in access control checks rather than relying on application-layer filtering alone.

Separation of Privileges divides security-critical operations across multiple components or actors, preventing any single compromise from achieving complete control. Financial transaction systems require approval from multiple parties for large transfers. Administrative operations require both authentication and a second factor. Cryptographic key management separates key generation, storage, and usage across different systems.

Economy of Mechanism prefers simple, auditable security implementations over complex systems. Complex security logic introduces opportunities for errors and increases the attack surface. A straightforward authentication system using established libraries and patterns provides better security than custom cryptographic implementations, regardless of developer intentions.

Open Design assumes attackers know system architecture and implementation details, forcing security to depend on keys and passwords rather than obscurity. Source code review, security audits, and penetration testing become more effective when security does not rely on hidden implementation details. Published security mechanisms allow community review and improvement.

# Multiple principles in practice
class SecureDocumentAccess
  def initialize(user, document_id)
    @user = user
    @document_id = document_id
  end

  def retrieve_document
    # Complete Mediation: Check authorization on every access
    authorize!
    
    # Fail Securely: Return nil rather than document on error
    document = Document.find_by(id: @document_id)
    return nil unless document
    
    # Defense in Depth: Additional validation
    return nil if document.deleted?
    return nil if document.requires_special_permission? && !@user.has_special_permission?
    
    # Least Privilege: Log access without exposing document content
    AuditLog.create(user_id: @user.id, document_id: @document_id, action: 'view')
    
    document
  rescue StandardError => e
    # Fail Securely: Log error but return nil
    ErrorLog.create(error: e.message, user_id: @user.id)
    nil
  end

  private

  def authorize!
    raise UnauthorizedError unless @user.can_access_document?(@document_id)
  end
end

Security Implications

Authentication Security forms the first line of defense in user-facing applications. Weak authentication allows attackers to impersonate legitimate users and access protected resources. Security by Design treats authentication as a critical architectural decision requiring careful implementation, not a feature added during development.

Modern authentication systems use adaptive hashing algorithms that increase computational cost as hardware improves. BCrypt, scrypt, and Argon2 provide this adaptability. Fixed-cost algorithms like SHA-256 become vulnerable as computing power increases. Password storage never uses reversible encryption or encoding. The system stores password hashes with unique salts, preventing rainbow table attacks and making password cracking computationally expensive even when attackers obtain database dumps.

Session management requires careful attention to session fixation attacks, session hijacking, and session lifetime management. Security by Design regenerates session identifiers after authentication, invalidates sessions on logout, implements absolute and idle timeouts, and stores session data server-side rather than in client cookies. Session identifiers contain sufficient entropy to prevent guessing attacks and never appear in URL parameters.

Authorization Architecture determines who can perform which operations on which resources. Flawed authorization logic represents one of the most common security vulnerabilities in web applications. Role-based access control (RBAC) provides coarse-grained permissions based on user roles. Attribute-based access control (ABAC) enables fine-grained decisions based on user attributes, resource properties, and environmental context.

Authorization checks occur at multiple layers: application controllers verify user permissions before invoking business logic, service layers enforce authorization rules within domain operations, and database queries filter results based on user context. This defense in depth prevents authorization bypass through direct data access or API manipulation.

Input Validation and Sanitization prevent injection attacks and data corruption. Every system boundary represents a potential attack vector where malicious input can compromise security. Web applications validate and sanitize data from HTTP parameters, headers, cookies, and file uploads. APIs validate JSON payloads against schemas. Database layers use parameterized queries to prevent SQL injection.

Validation occurs early in request processing, rejecting invalid input before it reaches business logic. Allowlist validation defines accepted patterns rather than denylisting dangerous characters. An email field accepts strings matching email patterns rather than attempting to filter out SQL injection characters. Numeric fields parse and validate ranges rather than accepting arbitrary strings.

Cryptographic Key Management determines the security of encrypted data and authenticated communications. Weak key management undermines strong encryption algorithms. Keys require secure generation using cryptographically secure random number generators, secure storage separate from encrypted data, regular rotation, and secure distribution mechanisms.

Applications separate encryption keys from application code, storing them in environment variables, key management services, or hardware security modules. Development and testing environments use different keys than production systems. Compromised development keys do not affect production data security.

Error Handling and Information Disclosure balance debugging capabilities with security requirements. Detailed error messages assist development but leak sensitive information to attackers. Production systems return generic error messages to clients while logging detailed information server-side. Stack traces, database errors, and file paths never appear in client-facing error responses.

Failed authentication attempts return identical responses for invalid usernames and invalid passwords, preventing username enumeration. Authorization failures return 404 Not Found rather than 403 Forbidden when appropriate, avoiding confirmation that protected resources exist.

# Secure error handling
class SecureController < ApplicationController
  rescue_from ActiveRecord::RecordNotFound do |e|
    # Don't reveal whether record exists
    Rails.logger.error("Record not found: #{e.message}")
    render json: { error: 'Resource not found' }, status: :not_found
  end

  rescue_from UnauthorizedError do |e|
    # Don't reveal authorization logic
    Rails.logger.error("Unauthorized access attempt: #{e.message}")
    render json: { error: 'Access denied' }, status: :forbidden
  end

  rescue_from StandardError do |e|
    # Never expose internal errors
    Rails.logger.error("Unexpected error: #{e.class} - #{e.message}\n#{e.backtrace.join("\n")}")
    render json: { error: 'An error occurred' }, status: :internal_server_error
  end
end

Ruby Implementation

Rails applications implement Security by Design through built-in security features and established gems. The framework provides CSRF protection, SQL injection prevention through Active Record, XSS protection through automatic output escaping, and secure session management. Additional security requires explicit implementation using these foundations.

Authentication Implementation typically uses the bcrypt gem for password hashing and Devise or custom solutions for session management. BCrypt provides adaptive hashing with configurable computational cost:

# config/initializers/bcrypt.rb
BCrypt::Engine.cost = Rails.env.production? ? 12 : 4

# app/models/user.rb
class User < ApplicationRecord
  has_secure_password
  
  validates :email, presence: true, uniqueness: true,
            format: { with: URI::MailTo::EMAIL_REGEXP }
  validates :password, length: { minimum: 12 },
            format: { with: /\A(?=.*[A-Z])(?=.*[0-9])/ },
            if: :password_digest_changed?
  
  def self.authenticate(email, password)
    user = find_by(email: email.downcase)
    return nil unless user
    return nil unless user.authenticate(password)
    
    # Check account status
    return nil if user.suspended?
    
    # Update last login
    user.update(last_login_at: Time.current)
    user
  end
end

# app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  def create
    user = User.authenticate(params[:email], params[:password])
    
    if user
      # Regenerate session to prevent fixation
      reset_session
      session[:user_id] = user.id
      session[:created_at] = Time.current
      redirect_to dashboard_path
    else
      # Same response for invalid username or password
      flash.now[:alert] = 'Invalid credentials'
      render :new
    end
  end

  def destroy
    reset_session
    redirect_to root_path
  end
end

Authorization Implementation uses gems like Pundit or CanCanCan for policy-based authorization. Pundit encourages explicit authorization policies:

# app/policies/document_policy.rb
class DocumentPolicy
  attr_reader :user, :document

  def initialize(user, document)
    @user = user
    @document = document
  end

  def show?
    document.public? || owner? || shared_with_user?
  end

  def update?
    owner? || collaborator?
  end

  def destroy?
    owner?
  end

  private

  def owner?
    document.owner_id == user.id
  end

  def collaborator?
    document.collaborators.exists?(user_id: user.id, role: 'editor')
  end

  def shared_with_user?
    document.shares.exists?(user_id: user.id)
  end
end

# app/controllers/documents_controller.rb
class DocumentsController < ApplicationController
  before_action :authenticate_user!
  
  def show
    @document = Document.find(params[:id])
    authorize @document
  end

  def update
    @document = Document.find(params[:id])
    authorize @document
    
    if @document.update(document_params)
      render json: @document
    else
      render json: { errors: @document.errors }, status: :unprocessable_entity
    end
  end
end

SQL Injection Prevention relies on parameterized queries through Active Record. Direct SQL requires careful parameter binding:

# Secure: Parameterized query
User.where("email = ? AND status = ?", params[:email], 'active')

# Secure: Hash conditions
User.where(email: params[:email], status: 'active')

# Insecure: String interpolation
User.where("email = '#{params[:email]}'") # NEVER DO THIS

# Secure: Raw SQL with parameters
sql = "SELECT * FROM users WHERE email = ? AND created_at > ?"
User.find_by_sql([sql, params[:email], 1.year.ago])

# Complex queries with sanitization
sanitized_column = ActiveRecord::Base.sanitize_sql(params[:sort_column])
allowed_columns = ['name', 'email', 'created_at']
column = allowed_columns.include?(sanitized_column) ? sanitized_column : 'created_at'
User.order(column)

Cross-Site Scripting (XSS) Prevention uses automatic HTML escaping in ERB templates. Manual HTML requires explicit sanitization:

# app/helpers/application_helper.rb
module ApplicationHelper
  def render_user_content(content)
    # Allowlist safe HTML tags
    sanitize(content, tags: %w[p br strong em ul ol li a],
             attributes: %w[href title])
  end

  def render_plain_text(text)
    simple_format(h(text))
  end
end

# app/views/posts/show.html.erb
<div class="post-content">
  <%= render_user_content(@post.content) %>
</div>

<div class="comment">
  <%= render_plain_text(@comment.text) %>
</div>

CSRF Protection comes enabled by default in Rails through token verification. API endpoints using token authentication disable CSRF protection:

class ApiController < ApplicationController
  skip_before_action :verify_authenticity_token
  before_action :authenticate_token!

  private

  def authenticate_token!
    token = request.headers['Authorization']&.split(' ')&.last
    decoded = JWT.decode(token, Rails.application.secret_key_base, true,
                        algorithm: 'HS256')
    @current_user = User.find(decoded[0]['user_id'])
  rescue JWT::DecodeError, ActiveRecord::RecordNotFound
    render json: { error: 'Invalid token' }, status: :unauthorized
  end
end

Secure File Upload validates file types, limits sizes, generates unique filenames, and stores files outside web root:

class DocumentUploader
  MAX_FILE_SIZE = 10.megabytes
  ALLOWED_CONTENT_TYPES = %w[
    application/pdf
    application/msword
    application/vnd.openxmlformats-officedocument.wordprocessingml.document
  ].freeze

  def initialize(file)
    @file = file
  end

  def upload
    validate_file!
    
    filename = generate_secure_filename
    storage_path = Rails.root.join('storage', 'documents', filename)
    
    File.open(storage_path, 'wb') do |f|
      f.write(@file.read)
    end
    
    { path: storage_path, filename: filename }
  end

  private

  def validate_file!
    raise ValidationError, 'File required' unless @file
    raise ValidationError, 'File too large' if @file.size > MAX_FILE_SIZE
    
    content_type = Marcel::MimeType.for(@file)
    unless ALLOWED_CONTENT_TYPES.include?(content_type)
      raise ValidationError, 'Invalid file type'
    end
  end

  def generate_secure_filename
    extension = File.extname(@file.original_filename)
    "#{SecureRandom.uuid}#{extension}"
  end
end

Design Considerations

Threat Modeling identifies potential attacks during design phases. STRIDE methodology categorizes threats: Spoofing, Tampering, Repudiation, Information Disclosure, Denial of Service, Elevation of Privilege. Each system component undergoes threat analysis to identify vulnerabilities before implementation.

Web applications face threats from SQL injection, XSS, CSRF, session hijacking, and authentication bypass. API services confront token theft, replay attacks, rate limiting bypass, and unauthorized access. Data storage systems protect against unauthorized access, data leakage, and integrity violations. Each threat requires specific mitigations designed into system architecture.

Trust Boundaries define points where data crosses from trusted to untrusted contexts or vice versa. External user input represents the most obvious trust boundary, but internal boundaries exist between services, between application tiers, and between security contexts. Each boundary requires validation, sanitization, and authorization checks appropriate to the risk.

A microservices architecture treats inter-service communication as crossing trust boundaries even within the same deployment. Service A cannot assume Service B's data remains valid or authorized. Each service validates inputs and enforces authorization independently. This defense in depth prevents cascading failures when one service becomes compromised.

Security Configuration Management centralizes security settings and prevents scattered security decisions across the codebase. Rails applications use environment variables for secrets, explicit security headers, and security-focused initializers:

# config/initializers/security.rb
Rails.application.config.action_controller.default_protect_from_forgery = true
Rails.application.config.force_ssl = true

# Security headers
Rails.application.config.action_dispatch.default_headers.merge!(
  'X-Frame-Options' => 'SAMEORIGIN',
  'X-Content-Type-Options' => 'nosniff',
  'X-XSS-Protection' => '1; mode=block',
  'Strict-Transport-Security' => 'max-age=31536000; includeSubDomains',
  'Content-Security-Policy' => "default-src 'self'; script-src 'self' 'unsafe-inline'"
)

# Session configuration
Rails.application.config.session_store :cookie_store,
  key: '_app_session',
  secure: Rails.env.production?,
  httponly: true,
  same_site: :lax,
  expire_after: 2.hours

Encryption Strategy determines what data requires encryption at rest and in transit. Sensitive personal information, financial data, authentication credentials, and personally identifiable information require encryption. The strategy specifies encryption algorithms, key management procedures, and key rotation policies.

Rails applications use Active Record Encryption for attribute-level encryption:

class User < ApplicationRecord
  encrypts :ssn, deterministic: false
  encrypts :email, deterministic: true # Allows unique indexes
  
  # Custom encryption context
  encrypts :credit_card, key_provider: CreditCardKeyProvider.new
end

Audit Logging records security-relevant events for investigation and compliance. Logs capture authentication attempts, authorization failures, data access, configuration changes, and security exceptions. Log entries include timestamps, user identifiers, affected resources, actions attempted, and outcomes. Logs themselves require protection against tampering and unauthorized access.

class AuditLogger
  def self.log_access(user:, resource:, action:, result:)
    AuditLog.create!(
      user_id: user&.id,
      user_email: user&.email,
      resource_type: resource.class.name,
      resource_id: resource.id,
      action: action,
      result: result,
      ip_address: Current.ip_address,
      user_agent: Current.user_agent,
      timestamp: Time.current
    )
  end
end

Practical Examples

Secure API Authentication demonstrates multiple security principles working together. The implementation uses JWT tokens with short expiration times, refresh tokens stored securely, rate limiting to prevent brute force attacks, and comprehensive audit logging:

class AuthenticationService
  TOKEN_EXPIRATION = 15.minutes
  REFRESH_TOKEN_EXPIRATION = 7.days

  def initialize(user)
    @user = user
  end

  def generate_tokens
    access_token = generate_access_token
    refresh_token = generate_refresh_token
    
    {
      access_token: access_token,
      refresh_token: refresh_token,
      expires_in: TOKEN_EXPIRATION.to_i
    }
  end

  def refresh_access_token(refresh_token)
    decoded = verify_refresh_token(refresh_token)
    return nil unless decoded
    
    stored_token = RefreshToken.find_by(
      token_digest: digest(refresh_token),
      user_id: @user.id
    )
    
    return nil unless stored_token&.valid?
    
    generate_access_token
  end

  private

  def generate_access_token
    payload = {
      user_id: @user.id,
      email: @user.email,
      roles: @user.roles,
      exp: TOKEN_EXPIRATION.from_now.to_i,
      iat: Time.current.to_i,
      jti: SecureRandom.uuid
    }
    
    JWT.encode(payload, Rails.application.secret_key_base, 'HS256')
  end

  def generate_refresh_token
    token = SecureRandom.urlsafe_base64(32)
    
    RefreshToken.create!(
      user_id: @user.id,
      token_digest: digest(token),
      expires_at: REFRESH_TOKEN_EXPIRATION.from_now
    )
    
    token
  end

  def verify_refresh_token(token)
    RefreshToken.find_by(
      token_digest: digest(token),
      user_id: @user.id
    )
  end

  def digest(token)
    Digest::SHA256.hexdigest(token)
  end
end

class RefreshToken < ApplicationRecord
  belongs_to :user
  
  validates :token_digest, presence: true, uniqueness: true
  validates :expires_at, presence: true
  
  def valid?
    !revoked? && expires_at > Time.current
  end
  
  def revoke!
    update!(revoked_at: Time.current)
  end
end

Multi-Factor Authentication adds a second authentication factor requiring users to provide something they have (authentication app code) in addition to something they know (password):

class MfaService
  def initialize(user)
    @user = user
  end

  def enable_mfa
    secret = ROTP::Base32.random
    @user.update!(
      mfa_secret: encrypt_secret(secret),
      mfa_enabled: false # Not enabled until verified
    )
    
    {
      secret: secret,
      qr_code: generate_qr_code(secret),
      backup_codes: generate_backup_codes
    }
  end

  def verify_and_enable(code)
    return false unless verify_code(code)
    
    @user.update!(mfa_enabled: true)
    true
  end

  def verify_code(code)
    totp = ROTP::TOTP.new(decrypt_secret(@user.mfa_secret))
    totp.verify(code, drift_behind: 30, drift_ahead: 30)
  end

  def verify_backup_code(code)
    backup_code = @user.backup_codes.find_by(
      code_digest: digest(code),
      used: false
    )
    
    return false unless backup_code
    
    backup_code.update!(used: true, used_at: Time.current)
    true
  end

  private

  def generate_backup_codes
    codes = 10.times.map { SecureRandom.hex(4).upcase }
    
    codes.each do |code|
      @user.backup_codes.create!(code_digest: digest(code))
    end
    
    codes
  end

  def generate_qr_code(secret)
    totp = ROTP::TOTP.new(secret, issuer: 'YourApp')
    RQRCode::QRCode.new(totp.provisioning_uri(@user.email)).as_svg
  end

  def encrypt_secret(secret)
    # Use Rails encrypted attributes or custom encryption
    ActiveSupport::MessageEncryptor.new(
      Rails.application.secret_key_base[0..31]
    ).encrypt_and_sign(secret)
  end

  def decrypt_secret(encrypted_secret)
    ActiveSupport::MessageEncryptor.new(
      Rails.application.secret_key_base[0..31]
    ).decrypt_and_verify(encrypted_secret)
  end

  def digest(code)
    Digest::SHA256.hexdigest(code)
  end
end

Secure Data Export handles sensitive data carefully during export operations, enforcing authorization, audit logging, and format sanitization:

class SecureDataExporter
  ALLOWED_FORMATS = %w[csv json].freeze
  RATE_LIMIT = 5.per_hour

  def initialize(user, resource_class, filters)
    @user = user
    @resource_class = resource_class
    @filters = filters
  end

  def export(format)
    validate_export!
    authorize_export!
    check_rate_limit!
    
    records = fetch_authorized_records
    sanitized_data = sanitize_records(records)
    
    log_export(records.count, format)
    
    case format
    when 'csv'
      generate_csv(sanitized_data)
    when 'json'
      generate_json(sanitized_data)
    end
  end

  private

  def validate_export!
    raise ArgumentError unless ALLOWED_FORMATS.include?(format)
    raise ArgumentError if @filters[:limit].to_i > 10000
  end

  def authorize_export!
    policy_class = "#{@resource_class}Policy".constantize
    raise UnauthorizedError unless policy_class.new(@user, @resource_class).export?
  end

  def check_rate_limit!
    key = "export:#{@user.id}"
    count = Rails.cache.increment(key, 1, expires_in: 1.hour)
    
    if count == 1
      Rails.cache.write(key, 1, expires_in: 1.hour)
    elsif count > RATE_LIMIT
      raise RateLimitError, 'Export limit exceeded'
    end
  end

  def fetch_authorized_records
    policy_scope = "#{@resource_class}Policy::Scope".constantize
    scope = policy_scope.new(@user, @resource_class).resolve
    
    scope.where(@filters.slice(:status, :category))
         .limit(@filters[:limit] || 1000)
  end

  def sanitize_records(records)
    records.map do |record|
      record.attributes.except(
        'password_digest',
        'mfa_secret',
        'api_token',
        'ssn',
        'credit_card'
      )
    end
  end

  def log_export(record_count, format)
    AuditLog.create!(
      user_id: @user.id,
      action: 'data_export',
      resource_type: @resource_class.name,
      details: {
        format: format,
        record_count: record_count,
        filters: @filters
      },
      ip_address: Current.ip_address
    )
  end

  def generate_csv(data)
    CSV.generate do |csv|
      csv << data.first.keys
      data.each { |row| csv << row.values }
    end
  end

  def generate_json(data)
    JSON.pretty_generate(data)
  end
end

Testing Approaches

Security Test Categories cover authentication, authorization, injection attacks, session management, and error handling. Tests verify both positive cases (authorized access succeeds) and negative cases (unauthorized access fails).

Authentication Testing verifies login mechanisms, password requirements, session creation, and account lockout:

RSpec.describe AuthenticationService do
  describe '#authenticate' do
    let(:user) { create(:user, password: 'SecurePass123!') }

    it 'authenticates with valid credentials' do
      result = AuthenticationService.authenticate(
        user.email,
        'SecurePass123!'
      )
      
      expect(result).to be_present
      expect(result.id).to eq(user.id)
    end

    it 'rejects invalid password' do
      result = AuthenticationService.authenticate(
        user.email,
        'WrongPassword'
      )
      
      expect(result).to be_nil
    end

    it 'rejects non-existent user' do
      result = AuthenticationService.authenticate(
        'nonexistent@example.com',
        'SecurePass123!'
      )
      
      expect(result).to be_nil
    end

    it 'locks account after failed attempts' do
      5.times do
        AuthenticationService.authenticate(user.email, 'wrong')
      end
      
      user.reload
      expect(user.locked?).to be true
      
      result = AuthenticationService.authenticate(
        user.email,
        'SecurePass123!'
      )
      expect(result).to be_nil
    end

    it 'rejects suspended accounts' do
      user.update(suspended: true)
      
      result = AuthenticationService.authenticate(
        user.email,
        'SecurePass123!'
      )
      
      expect(result).to be_nil
    end
  end
end

Authorization Testing verifies policy enforcement across different user roles and ownership scenarios:

RSpec.describe DocumentPolicy do
  let(:owner) { create(:user) }
  let(:collaborator) { create(:user) }
  let(:other_user) { create(:user) }
  let(:document) { create(:document, owner: owner) }

  describe '#show?' do
    it 'allows owner to view' do
      policy = DocumentPolicy.new(owner, document)
      expect(policy.show?).to be true
    end

    it 'allows collaborators to view' do
      document.collaborators.create(user: collaborator, role: 'viewer')
      policy = DocumentPolicy.new(collaborator, document)
      expect(policy.show?).to be true
    end

    it 'denies unauthorized users' do
      policy = DocumentPolicy.new(other_user, document)
      expect(policy.show?).to be false
    end

    it 'allows public documents to all' do
      document.update(public: true)
      policy = DocumentPolicy.new(other_user, document)
      expect(policy.show?).to be true
    end
  end

  describe '#update?' do
    it 'allows owner to update' do
      policy = DocumentPolicy.new(owner, document)
      expect(policy.update?).to be true
    end

    it 'allows editor collaborators to update' do
      document.collaborators.create(user: collaborator, role: 'editor')
      policy = DocumentPolicy.new(collaborator, document)
      expect(policy.update?).to be true
    end

    it 'denies viewer collaborators' do
      document.collaborators.create(user: collaborator, role: 'viewer')
      policy = DocumentPolicy.new(collaborator, document)
      expect(policy.update?).to be false
    end
  end
end

SQL Injection Testing attempts injection attacks against database queries:

RSpec.describe 'SQL Injection Protection' do
  it 'prevents injection in WHERE clauses' do
    malicious_input = "' OR '1'='1"
    
    expect {
      User.where("email = '#{malicious_input}'").to_a
    }.to raise_error(ActiveRecord::StatementInvalid)
    
    # Safe parameterized version
    result = User.where("email = ?", malicious_input)
    expect(result).to be_empty
  end

  it 'prevents injection in ORDER BY' do
    malicious_input = "name; DROP TABLE users--"
    
    # Unsafe version would fail
    # User.order(malicious_input)
    
    # Safe version with allowlist
    allowed_columns = %w[name email created_at]
    column = allowed_columns.include?(params[:sort]) ? params[:sort] : 'created_at'
    expect { User.order(column).to_a }.not_to raise_error
  end

  it 'sanitizes LIKE queries' do
    malicious_input = "%'; DROP TABLE users--"
    
    result = User.where("name LIKE ?", "%#{User.sanitize_sql_like(malicious_input)}%")
    expect(result).to be_empty
    expect(User.count).to be > 0 # Table still exists
  end
end

XSS Testing verifies output encoding prevents script injection:

RSpec.describe 'XSS Protection' do
  it 'escapes HTML in user content' do
    post = create(:post, content: '<script>alert("XSS")</script>')
    
    rendered = render_template('posts/show', post: post)
    
    expect(rendered).to include('&lt;script&gt;')
    expect(rendered).not_to include('<script>')
  end

  it 'allows safe HTML tags' do
    post = create(:post, content: '<p>Safe <strong>content</strong></p>')
    
    rendered = render_template('posts/show', post: post)
    
    expect(rendered).to include('<p>')
    expect(rendered).to include('<strong>')
  end

  it 'strips dangerous attributes' do
    post = create(:post, content: '<a href="javascript:alert()">Click</a>')
    
    rendered = render_template('posts/show', post: post)
    
    expect(rendered).not_to include('javascript:')
  end
end

Session Security Testing validates session management implementation:

RSpec.describe 'Session Security' do
  it 'regenerates session on login' do
    old_session_id = session.id
    
    post '/login', params: { email: user.email, password: 'password' }
    
    expect(session.id).not_to eq(old_session_id)
  end

  it 'expires sessions after timeout' do
    login_as(user)
    
    travel 3.hours do
      get '/dashboard'
      expect(response).to redirect_to(login_path)
    end
  end

  it 'invalidates session on logout' do
    login_as(user)
    session_id = session.id
    
    delete '/logout'
    
    # Session should be cleared
    expect(session[:user_id]).to be_nil
  end
end

Common Pitfalls

Mass Assignment Vulnerabilities occur when applications allow users to set any model attribute through parameter binding. Without strong parameters, attackers modify sensitive fields like admin flags or user roles:

# Vulnerable
def create
  User.create(params[:user]) # Allows any attribute
end

# Secure
def create
  User.create(user_params)
end

private

def user_params
  params.require(:user).permit(:email, :password, :name)
end

Timing Attacks exploit response time differences to extract information. Comparing passwords or tokens with standard equality checks reveals information through timing:

# Vulnerable to timing attacks
def valid_token?(provided, expected)
  provided == expected # Early exit on first mismatch
end

# Secure constant-time comparison
def valid_token?(provided, expected)
  ActiveSupport::SecurityUtils.secure_compare(provided, expected)
end

Insecure Direct Object References expose internal object identifiers allowing unauthorized access through ID manipulation:

# Vulnerable
def show
  @document = Document.find(params[:id]) # No authorization check
end

# Secure
def show
  @document = current_user.documents.find(params[:id])
  # Or use authorization policy
  authorize @document
end

Insufficient Session Expiration allows sessions to remain valid indefinitely, increasing exposure window if session tokens get compromised:

# Vulnerable
Rails.application.config.session_store :cookie_store,
  key: '_app_session'

# Secure
Rails.application.config.session_store :cookie_store,
  key: '_app_session',
  expire_after: 2.hours

Weak Random Number Generation uses predictable pseudo-random values for security-critical operations like tokens or session identifiers:

# Vulnerable
reset_token = Random.rand(1000000).to_s

# Secure
reset_token = SecureRandom.urlsafe_base64(32)

Logging Sensitive Data writes passwords, tokens, or personal information to log files:

# Vulnerable
Rails.logger.info("User logged in: #{user.email} with password #{params[:password]}")

# Secure
Rails.application.config.filter_parameters += [:password, :token, :ssn, :credit_card]
Rails.logger.info("User logged in: #{user.email}")

Hardcoded Secrets embed API keys, passwords, or encryption keys in source code:

# Vulnerable
API_KEY = "sk_live_abc123xyz789"

# Secure
API_KEY = ENV['API_KEY'] || raise('API_KEY not configured')

XML External Entity (XXE) Attacks occur when parsing XML input without disabling external entity resolution:

# Vulnerable
doc = Nokogiri::XML(params[:xml_data])

# Secure
doc = Nokogiri::XML(params[:xml_data]) do |config|
  config.noent.nonet
end

Unrestricted File Upload accepts any file type without validation, allowing executable uploads:

# Vulnerable
def upload
  File.write("uploads/#{params[:file].original_filename}", params[:file].read)
end

# Secure
def upload
  uploader = SecureFileUploader.new(params[:file])
  uploader.validate_type!
  uploader.validate_size!
  uploader.save_with_unique_name
end

Broken Access Control fails to verify authorization after authentication:

# Vulnerable
def update
  @user = User.find(params[:id])
  @user.update(user_params) # Any authenticated user can update any user
end

# Secure
def update
  @user = User.find(params[:id])
  authorize @user # Verify current user can update this user
  @user.update(user_params)
end

Reference

Security Principles

Principle Description Implementation
Least Privilege Minimum necessary permissions Role-based access control, permission scoping
Defense in Depth Multiple security layers Input validation, parameterized queries, output encoding
Fail Securely Default to deny on errors Return nil on errors, generic error messages
Complete Mediation Check authorization on every access Authorization checks per request
Separation of Privileges Divide critical operations Multi-factor authentication, approval workflows
Economy of Mechanism Prefer simple security Use established libraries, avoid custom crypto
Open Design Security not based on secrecy Assume attackers know implementation

Common Vulnerabilities

Vulnerability Risk Mitigation
SQL Injection Data breach, data loss Parameterized queries, ORM usage
Cross-Site Scripting Session hijacking, phishing Output encoding, CSP headers
CSRF Unauthorized actions CSRF tokens, SameSite cookies
Authentication Bypass Unauthorized access Strong password hashing, MFA
Session Fixation Session hijacking Regenerate session on login
Mass Assignment Privilege escalation Strong parameters
Insecure Direct Object References Unauthorized data access Authorization checks
XXE Attacks File disclosure, SSRF Disable external entities

Ruby Security Gems

Gem Purpose Usage
bcrypt Password hashing has_secure_password
devise Authentication framework User authentication, session management
pundit Authorization policies Policy-based access control
cancancan Authorization rules Rule-based authorization
rack-attack Rate limiting Throttle requests, prevent brute force
brakeman Static security analysis Security vulnerability scanning
bundler-audit Dependency checking Check for vulnerable gems
secure_headers Security headers CSP, HSTS configuration

Security Headers

Header Purpose Recommended Value
Content-Security-Policy Prevent XSS default-src 'self'
Strict-Transport-Security Force HTTPS max-age=31536000; includeSubDomains
X-Frame-Options Prevent clickjacking SAMEORIGIN
X-Content-Type-Options Prevent MIME sniffing nosniff
X-XSS-Protection XSS filtering 1; mode=block
Referrer-Policy Control referrer strict-origin-when-cross-origin

Authentication Configuration

Setting Recommendation Rationale
Password minimum length 12 characters Resist brute force attacks
Password complexity Upper, lower, number Increase entropy
Hashing algorithm BCrypt cost 12+ Adaptive computational cost
Session timeout 2 hours absolute Limit exposure window
MFA enforcement Required for admins Additional authentication factor
Account lockout 5 failed attempts Prevent brute force
Token expiration 15 minutes Short-lived access tokens

Authorization Patterns

Pattern Use Case Implementation
Role-Based Access Control Predefined roles User has roles, roles have permissions
Attribute-Based Access Control Complex rules Evaluate user/resource attributes
Owner-Based User owns resources Check resource owner_id equals user_id
Team-Based Shared resources Check user belongs to resource team
Time-Based Temporary access Check access expiration timestamp
Location-Based Geographic restrictions Validate IP address or location

Encryption Standards

Use Case Algorithm Key Size
Password hashing BCrypt N/A (adaptive)
Data at rest AES-GCM 256 bits
Data in transit TLS 1.3 2048+ bits RSA
Token signing HMAC-SHA256 256 bits
Symmetric encryption AES-CBC 256 bits
Asymmetric encryption RSA 2048+ bits

Input Validation Rules

Input Type Validation Sanitization
Email Regex format check Strip whitespace, lowercase
URL URI parsing Remove javascript: protocol
HTML Allowlist tags Strip dangerous tags/attributes
SQL Parameterized queries Use ORM, escape LIKE wildcards
File paths Allowlist directory Reject path traversal
Numeric Parse and range check Convert to integer/float
JSON Schema validation Parse with safe parser