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