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
-
.safe_const_get(class_name) ⇒ Class, Module
Resolve a validated class name to its Class object.
Class Method Details
.safe_const_get(class_name) ⇒ Class, Module
Resolve a validated class name to its Class object.
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.}" 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 |