Skip to content

Extras: Metaprogramming

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

Metaprogramming Security Risks

Metaprogramming is a powerful feature in Ruby that allows code to dynamically manipulate itself at runtime. While this enables elegant and flexible solutions, it also introduces significant security risks when user input is involved.

What is Metaprogramming?

Metaprogramming allows programs to:

  • Dynamically define or modify classes and methods
  • Invoke methods by name using strings or symbols
  • Access and modify object internals
  • Generate code at runtime

Common Dangerous Patterns

Ruby and Rails provide several metaprogramming methods that become dangerous when combined with user input:

1. Constantize

Converts strings to constant references (classes/modules), allowing attackers to instantiate arbitrary classes.

Example:

# Dangerous
params[:type].constantize.new  # User can instantiate ANY class

Read Full Constantize Documentation →

2. Send Method

Dynamically invokes methods on objects, allowing attackers to call any method including private ones.

Example:

# Dangerous
user.send(params[:method])  # User can call ANY method

Read Full Send Method Documentation →

3. Other Dangerous Methods

Additional metaprogramming methods to be cautious with:

  • eval: Executes arbitrary Ruby code

    eval(params[:code])  # NEVER do this - direct code execution
  • instance_eval / class_eval: Evaluates code in object/class context

    @user.instance_eval(params[:code])  # Dangerous
  • define_method: Dynamically creates methods

    define_method(params[:name]) { }  # Can create unwanted methods
  • const_get: Similar to constantize

    Object.const_get(params[:class])  # Can access any constant
  • method: Returns a method object that can be called

    @user.method(params[:method_name]).call  # Similar to send
  • instance_variable_get/set: Direct instance variable access

    @user.instance_variable_set(params[:var], params[:value])  # Bypass validations

Why Metaprogramming + User Input = Danger

When user input controls metaprogramming operations:

  1. Bypassed Access Controls: Private/protected methods become accessible
  2. Unexpected Behavior: Code paths you never intended get executed
  3. Data Exposure: Sensitive attributes and methods become accessible
  4. Code Execution: In severe cases, arbitrary code can be executed
  5. Business Logic Bypass: Validations and security checks can be circumvented

The Attack Pattern

Most metaprogramming vulnerabilities follow this pattern:

# 1. Application expects specific, safe input
expected_input = "safe_method"

# 2. User provides malicious input instead
actual_input = params[:input]  # "dangerous_method" or "admin="

# 3. Metaprogramming executes user-controlled code
object.send(actual_input)  # Oops - executed dangerous_method!

Defense Strategies

1. Avoid Metaprogramming with User Input

The best defense is not using metaprogramming with user input at all:

# Instead of:
object.send(params[:method])

# Use explicit code:
case params[:method]
when 'name' then object.name
when 'email' then object.email
else raise "Invalid method"
end

2. Strict Whitelisting

If you must use metaprogramming, use strict whitelists:

ALLOWED_METHODS = %w[name email created_at].freeze

if ALLOWED_METHODS.include?(params[:method])
  object.send(params[:method])
else
  raise "Unauthorized method"
end

3. Input Validation

Validate all user input before using in metaprogramming:

# Check format
unless params[:class].match?(/\A[A-Z][a-zA-Z0-9]*\z/)
  raise "Invalid class name"
end

# Check whitelist
ALLOWED_CLASSES = %w[User Post Comment].freeze
unless ALLOWED_CLASSES.include?(params[:class])
  raise "Unauthorized class"
end

# Now safe(r) to use
klass = params[:class].constantize

4. Use Framework Protections

Rails provides built-in protections - use them:

# Strong Parameters
def user_params
  params.require(:user).permit(:name, :email)
end

# Instead of:
user.send("#{params[:attr]}=", params[:value])

# Use:
user.update(user_params)

5. Least Privilege Principle

Design your code to minimize metaprogramming needs:

  • Use explicit method calls when possible
  • Create specific API endpoints instead of generic ones
  • Limit dynamic behavior to safe, well-tested contexts
  • Default to static code, use metaprogramming sparingly

RailsGoat Examples

RailsGoat demonstrates metaprogramming vulnerabilities in:

  1. Benefit Forms Controller - Unchecked constantize usage

  2. Other Controllers - Various metaprogramming anti-patterns throughout the application

Testing for Metaprogramming Vulnerabilities

When testing applications, look for:

  1. User input in metaprogramming methods

    • Search for: constantize, send, eval, instance_eval
    • Check if parameters flow into these methods
  2. Dynamic method/class resolution

    • Look for patterns like: params[:type].constantize
    • Check for: object.send(params[:method])
  3. Missing input validation

    • Are there whitelists for user-controlled values?
    • Can users provide arbitrary strings to metaprogramming methods?
  4. Privilege escalation opportunities

    • Can users call admin-only methods?
    • Can users modify role/permission attributes?

Key Takeaways

  1. Metaprogramming is powerful but dangerous with user input
  2. Always whitelist user input before metaprogramming operations
  3. Prefer explicit code over dynamic code when possible
  4. Defense in depth: Validate input, check permissions, log access
  5. Test thoroughly - try to break your own whitelists

Further Reading

Practice

To understand these vulnerabilities hands-on:

  1. Set up RailsGoat locally
  2. Navigate to the Benefit Forms section
  3. Try attacking the constantize vulnerability
  4. Review the vulnerable code
  5. Implement the suggested fixes
  6. Verify the fixes prevent the attacks

Remember: RailsGoat is intentionally vulnerable for educational purposes. Never deploy it to production!

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