@@ -68,20 +68,68 @@ def parse_payload(raw_body, headers, symbolize: true)
6868 # @raise [LoadError] If the handler file or class cannot be found
6969 # @raise [StandardError] Halts with error if handler cannot be loaded
7070 def load_handler ( handler_class_name , handler_dir )
71+ # Security: Validate handler class name to prevent arbitrary class loading
72+ unless valid_handler_class_name? ( handler_class_name )
73+ error! ( "invalid handler class name: #{ handler_class_name } " , 400 )
74+ end
75+
7176 # Convert class name to file name (e.g., Team1Handler -> team1_handler.rb)
7277 # E.g.2: GithubHandler -> github_handler.rb
7378 # E.g.3: GitHubHandler -> git_hub_handler.rb
7479 file_name = handler_class_name . gsub ( /([A-Z])/ , '_\1' ) . downcase . sub ( /^_/ , "" ) + ".rb"
7580 file_path = File . join ( handler_dir , file_name )
7681
82+ # Security: Ensure the file path doesn't escape the handler directory
83+ normalized_handler_dir = Pathname . new ( File . expand_path ( handler_dir ) )
84+ normalized_file_path = Pathname . new ( File . expand_path ( file_path ) )
85+ unless normalized_file_path . descend . any? { |path | path == normalized_handler_dir }
86+ error! ( "handler path outside of handler directory" , 400 )
87+ end
88+
7789 if File . exist? ( file_path )
7890 require file_path
79- Object . const_get ( handler_class_name ) . new
91+ handler_class = Object . const_get ( handler_class_name )
92+
93+ # Security: Ensure the loaded class inherits from the expected base class
94+ unless handler_class < Hooks ::Handlers ::Base
95+ error! ( "handler class must inherit from Hooks::Handlers::Base" , 400 )
96+ end
97+
98+ handler_class . new
8099 else
81100 raise LoadError , "Handler #{ handler_class_name } not found at #{ file_path } "
82101 end
83102 rescue => e
84- error! ( "failed to load handler #{ handler_class_name } : #{ e . message } " , 500 )
103+ error! ( "failed to load handler: #{ e . message } " , 500 )
104+ end
105+
106+ private
107+
108+ # Validate that a handler class name is safe to load
109+ #
110+ # @param class_name [String] The class name to validate
111+ # @return [Boolean] true if the class name is safe, false otherwise
112+ def valid_handler_class_name? ( class_name )
113+ # Must be a string
114+ return false unless class_name . is_a? ( String )
115+
116+ # Must not be empty or only whitespace
117+ return false if class_name . strip . empty?
118+
119+ # Must match a safe pattern: alphanumeric + underscore, starting with uppercase
120+ # Examples: MyHandler, GitHubHandler, Team1Handler
121+ return false unless class_name . match? ( /\A [A-Z][a-zA-Z0-9_]*\z / )
122+
123+ # Must not be a system/built-in class name
124+ dangerous_classes = %w[
125+ File Dir Kernel Object Class Module Proc Method
126+ IO Socket TCPSocket UDPSocket BasicSocket
127+ Process Thread Fiber Mutex ConditionVariable
128+ Marshal YAML JSON Pathname
129+ ]
130+ return false if dangerous_classes . include? ( class_name )
131+
132+ true
85133 end
86134
87135 # Determine HTTP error code from exception
0 commit comments