Rails: Looking up constants by their name string

Updated . Posted . Visible to the public.

TL;DR: Rails ships two methods to convert strings to constants, constantize and safe_constantize. Neither is safe for untrusted user input. Before you call either method you must validate the input string against an allowlist. The only difference between the two methods is that unresolvable constants raise an error with constantize, but return nil with safe_constantize. If you validate the input string against an allowlist, an error should never happen.

Preventing Dangerous Lookups

Suppose an application uses either constantize or safe_constantize to dynamically resolve class names based on user input:

# This is NOT!! safe, even though we used `safe_constantize`
user_input = params[:class_name]
user_input.safe_constantize.new

An attacker could craft a request with a malicious class_name such as Kernel, enabling them to execute dangerous methods like Kernel.system("rm -rf /"). This can lead to severe vulnerabilities.

By using an allowlist, the application ensures that only valid and expected constants are resolved:

class_name = params[:type].presence_in(%w[User Post Test])
if class_name
  class_name.safe_constantize.new #  either User, Post or Test
else
  Rails.logger.error "This should not happen!"
end

Handling unresolvable constants

The safe_constantize method is defined in the ActiveSupport library and is used to convert a string into a constant. If the string does not correspond to a valid constant or if the constant is not accessible, it returns nil:

"String".safe_constantize
# => String

"NonExistentClass".safe_constantize
# => nil

"Module::ExistingClass".safe_constantize
# => Module::ExistingClass

"Invalid::Constant".safe_constantize
# => nil

In contrast, using the constantize method without the safe_ prefix would raise an exception if the string cannot be resolved to a constant:

"NonExistentClass".constantize
# => NameError: uninitialized constant NonExistentClass

Dynamic Programming

In scenarios where your application’s behavior depends on dynamic constant resolution, safe_constantize may offer a more convenient control flow to handle invalid or nonexistent constants. Again, remember that despite its name, safe_constantize is not safe for untrusted user input and must be validated against an allowlist.

class JobProcessor
  self << class
    def process(job_type)
      job_class = job_type.presence_in? allowed_jobs
  
      if job_class
        job_class.safe_constantize.perform
      else
        Rails.logger.error("Invalid job type: \#{job_type}")
      end
    end
    
    private 
    
    def allowed_jobs
      %w[BaseJob EmailJob]
    end
  end
end

class BaseJob
  def self.perform
    puts "Performing base job"
  end
end

class EmailJob < BaseJob
  def self.perform
    puts "Sending email"
  end
end

JobProcessor.process("EmailJob")
# Output: Sending email

JobProcessor.process("NonExistentJob")
# Logs error: Invalid job type: NonExistentJob
Felix Eschey
Last edit
Henning Koch
License
Source code in this card is licensed under the MIT License.
Posted by Felix Eschey to makandra dev (2024-12-13 10:31)