Module: Otto::Security::ConstantResolver

Defined in:
lib/otto/security/constant_resolver.rb

Overview

Shared, validated resolution of a class from its String name.

Centralizes the class-name format check and the forbidden-class blocklist so every dispatch path that turns a route/handler string into a constant (Otto::Route, RouteHandlers::BaseHandler, and the MCP registry/server) enforces the SAME guards against code-injection via crafted class names.

Constant Summary collapse

CLASS_NAME_PATTERN =

A class name is a sequence of ::-separated, capitalized Ruby constant tokens. This also rejects leading “::” (a name must start with [A-Z]).

/\A[A-Z][a-zA-Z0-9_]*(?:::[A-Z][a-zA-Z0-9_]*)*\z/
FORBIDDEN_CLASSES =

Constants that must never be resolvable from untrusted route/handler strings, since dispatching to them enables arbitrary/dangerous behavior.

%w[
  Kernel Module Class Object BasicObject
  File Dir IO Process System
  Binding Proc Method UnboundMethod
  Thread ThreadGroup Fiber
  ObjectSpace GC
].freeze
FORBIDDEN_CONSTANTS =

The actual constant objects behind FORBIDDEN_CLASSES that exist in this runtime. The resolved constant is checked against these by identity so a forbidden class reached through a namespace prefix (e.g. “Object::Kernel”) or via Ruby’s trailing-segment constant inheritance (e.g. “App::File” falling back to top-level ::File) is rejected even though its literal string is not listed in FORBIDDEN_CLASSES. An app’s OWN class that merely shares a name (a distinct object) is unaffected.

FORBIDDEN_CLASSES.filter_map do |const_name|
  Object.const_get(const_name) if Object.const_defined?(const_name, false)
end.freeze

Class Method Summary collapse

Class Method Details

.safe_const_get(class_name) ⇒ Class, Module

Resolve a validated class name to its Class object.

Parameters:

  • class_name (String)

    fully-qualified class name (e.g. “App::Users”)

Returns:

  • (Class, Module)

    the resolved constant

Raises:

  • (ArgumentError)

    if the name is malformed, forbidden, or not found



46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/otto/security/constant_resolver.rb', line 46

def safe_const_get(class_name)
  name = class_name.to_s

  raise ArgumentError, "Invalid class name format: #{class_name}" unless name.match?(CLASS_NAME_PATTERN)

  raise ArgumentError, "Forbidden class name: #{class_name}" if FORBIDDEN_CLASSES.include?(name)

  fq_class_name = "::#{name.sub(/^::+/, '')}"

  resolved =
    begin
      Object.const_get(fq_class_name)
    rescue NameError => e
      raise ArgumentError, "Class not found: #{fq_class_name} - #{e.message}"
    end

  # Reject forbidden constants reached via a namespace prefix or Ruby's
  # trailing-segment constant inheritance, which the literal-name check
  # above cannot see (e.g. "Object::Kernel", or "App::File" -> ::File).
  if FORBIDDEN_CONSTANTS.any? { |forbidden| resolved.equal?(forbidden) }
    raise ArgumentError, "Forbidden class name: #{class_name}"
  end

  resolved
end