Skip to content

Extras: Constantize

Ken Johnson edited this page Dec 9, 2025 · 2 revisions

Description

The constantize method is a Rails metaprogramming method that converts a string into a constant reference (typically a class or module). For example, "User".constantize returns the User class. This is commonly used to dynamically instantiate classes based on runtime conditions.

The Security Risk: When user-supplied input is passed to constantize, an attacker can instantiate any accessible class in the application, including dangerous system classes. This can lead to arbitrary code execution, information disclosure, or denial of service.

Bug

Within the file app/controllers/benefit_forms_controller.rb:

def download
  begin
    path = params[:name]
    file = params[:type].constantize.new(path)
    send_file file, disposition: "attachment"
  rescue
    redirect_to user_benefit_forms_path(user_id: current_user.id)
  end
end

This code contains TWO separate vulnerabilities:

Vulnerability 1: Unchecked Constantize (Primary Issue)

file = params[:type].constantize.new(path)

The params[:type] value is passed directly to constantize without validation. This allows an attacker to:

  • Instantiate any Ruby class accessible in the application
  • Pass arbitrary arguments to the class constructor
  • Potentially trigger dangerous behavior in class initialization

Expected behavior: The application expects params[:type] to be "File" Actual behavior: An attacker can specify ANY class name

Vulnerability 2: Path Traversal

path = params[:name]

The filename is taken directly from user input without validation, allowing path traversal attacks (e.g., ../../etc/passwd). While serious, this is a separate issue from the constantize vulnerability.

Attack Examples

Attack 1: Information Disclosure via Pathname Class

Instead of instantiating File, an attacker can instantiate Ruby's Pathname class which, when converted to a string by send_file, reveals file paths:

http://railsgoat.dev/download?name=/etc/passwd&type=Pathname

This bypasses the intended file download mechanism and can expose sensitive file paths.

Attack 2: Code Execution via ERB Template

An attacker can instantiate ERB with malicious template code:

http://railsgoat.dev/download?name=<%=`whoami`%>&type=ERB

When send_file attempts to process the ERB object, the template code may be evaluated, executing arbitrary commands.

Attack 3: Denial of Service

An attacker can instantiate classes with expensive constructors or cause application errors:

http://railsgoat.dev/download?name=malicious_data&type=ActiveRecord::Base

This can crash the application or consume excessive resources.

Why the Old Example Was Confusing

The original documentation showed:

http://railsgoat.dev/download?name=|touch+testthis.txt&type=Logger

This example was problematic because:

  1. It focused on command injection through the name parameter (path traversal issue)
  2. The Logger class doesn't execute shell commands from its constructor
  3. It didn't clearly demonstrate why constantize itself is dangerous
  4. It mixed two separate vulnerabilities, making both harder to understand

Solution

Insecure Code (Current)

def download
  begin
    path = params[:name]
    file = params[:type].constantize.new(path)
    send_file file, disposition: "attachment"
  rescue
    redirect_to user_benefit_forms_path(user_id: current_user.id)
  end
end

Secure Code (Fixed)

def download
  # Define allowed files with a whitelist approach
  ALLOWED_FILES = {
    "health" => "Health_n_Stuff.pdf",
    "dental" => "Dental_n_Stuff.pdf"
  }.freeze

  # Define allowed types (though we only need File here)
  ALLOWED_TYPES = ["File"].freeze

  begin
    # Validate the file identifier
    unless ALLOWED_FILES.key?(params[:name])
      flash[:error] = "Invalid file requested"
      redirect_to user_benefit_forms_path(user_id: current_user.id) and return
    end

    # Validate the type parameter
    unless ALLOWED_TYPES.include?(params[:type])
      flash[:error] = "Invalid file type"
      redirect_to user_benefit_forms_path(user_id: current_user.id) and return
    end

    # Use validated values
    filename = ALLOWED_FILES[params[:name]]
    path = Rails.root.join('public', 'docs', filename)

    # No need for constantize here - just use File directly
    send_file path, disposition: "attachment"
  rescue => e
    Rails.logger.error "File download error: #{e.message}"
    flash[:error] = "File not found"
    redirect_to user_benefit_forms_path(user_id: current_user.id)
  end
end

Key Security Improvements

  1. Whitelist Approach: Only allow specific, predefined file identifiers
  2. Remove Constantize: Eliminate dynamic class instantiation entirely - just use File directly via send_file
  3. Path Construction: Build safe paths using Rails.root.join with validated filename
  4. Input Validation: Check all user inputs against allowed values before use
  5. Proper Error Handling: Log errors without exposing internal details to users

Best Practices for Constantize

If you must use constantize (which should be rare):

  1. Use a whitelist: Only allow specific, known-safe class names

    ALLOWED_CLASSES = ["User", "Post", "Comment"].freeze
    klass = ALLOWED_CLASSES.include?(params[:type]) ? params[:type].constantize : nil
  2. Namespace restrictions: Limit to specific namespaces

    if params[:type].start_with?("Safe::")
      klass = params[:type].constantize
    end
  3. Avoid if possible: Consider alternative designs that don't require dynamic class instantiation

Hint

When downloading benefit forms, users should only be able to access predefined, legitimate documents. Ask yourself:

  • Why would users need to specify the class type?
  • Why not use a simple ID-to-filename mapping?
  • What classes could an attacker instantiate to cause harm?

Sections are divided by their OWASP Top Ten label (A1-A10) and marked as R4 and R5 for Rails 4 and 5.

Clone this wiki locally