-
Notifications
You must be signed in to change notification settings - Fork 0
Security audit: Fix critical authentication bypass and class injection vulnerabilities #10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
986e842
ef89962
a9e1fc4
49c8b75
465af64
aa0d8f0
1fea6c0
9732735
c902335
9c0cc49
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -68,20 +68,68 @@ def parse_payload(raw_body, headers, symbolize: true) | |
# @raise [LoadError] If the handler file or class cannot be found | ||
# @raise [StandardError] Halts with error if handler cannot be loaded | ||
def load_handler(handler_class_name, handler_dir) | ||
# Security: Validate handler class name to prevent arbitrary class loading | ||
unless valid_handler_class_name?(handler_class_name) | ||
error!("invalid handler class name: #{handler_class_name}", 400) | ||
end | ||
|
||
# Convert class name to file name (e.g., Team1Handler -> team1_handler.rb) | ||
# E.g.2: GithubHandler -> github_handler.rb | ||
# E.g.3: GitHubHandler -> git_hub_handler.rb | ||
file_name = handler_class_name.gsub(/([A-Z])/, '_\1').downcase.sub(/^_/, "") + ".rb" | ||
file_path = File.join(handler_dir, file_name) | ||
|
||
# Security: Ensure the file path doesn't escape the handler directory | ||
normalized_handler_dir = Pathname.new(File.expand_path(handler_dir)) | ||
normalized_file_path = Pathname.new(File.expand_path(file_path)) | ||
unless normalized_file_path.descend.any? { |path| path == normalized_handler_dir } | ||
error!("handler path outside of handler directory", 400) | ||
end | ||
|
||
if File.exist?(file_path) | ||
require file_path | ||
Object.const_get(handler_class_name).new | ||
handler_class = Object.const_get(handler_class_name) | ||
|
||
# Security: Ensure the loaded class inherits from the expected base class | ||
unless handler_class < Hooks::Handlers::Base | ||
error!("handler class must inherit from Hooks::Handlers::Base", 400) | ||
end | ||
|
||
handler_class.new | ||
else | ||
raise LoadError, "Handler #{handler_class_name} not found at #{file_path}" | ||
end | ||
rescue => e | ||
error!("failed to load handler #{handler_class_name}: #{e.message}", 500) | ||
error!("failed to load handler: #{e.message}", 500) | ||
end | ||
|
||
private | ||
|
||
# Validate that a handler class name is safe to load | ||
# | ||
# @param class_name [String] The class name to validate | ||
# @return [Boolean] true if the class name is safe, false otherwise | ||
def valid_handler_class_name?(class_name) | ||
# Must be a string | ||
return false unless class_name.is_a?(String) | ||
|
||
# Must not be empty or only whitespace | ||
return false if class_name.strip.empty? | ||
|
||
# Must match a safe pattern: alphanumeric + underscore, starting with uppercase | ||
# Examples: MyHandler, GitHubHandler, Team1Handler | ||
return false unless class_name.match?(/\A[A-Z][a-zA-Z0-9_]*\z/) | ||
|
||
# Must not be a system/built-in class name | ||
dangerous_classes = %w[ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [nitpick] The Copilot uses AI. Check for mistakes. Positive FeedbackNegative Feedback |
||
File Dir Kernel Object Class Module Proc Method | ||
IO Socket TCPSocket UDPSocket BasicSocket | ||
Process Thread Fiber Mutex ConditionVariable | ||
Marshal YAML JSON Pathname | ||
] | ||
return false if dangerous_classes.include?(class_name) | ||
|
||
true | ||
end | ||
|
||
# Determine HTTP error code from exception | ||
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -60,7 +60,7 @@ def self.validate_global_config(config) | |||||||||||||||||
result.to_h | ||||||||||||||||||
end | ||||||||||||||||||
|
||||||||||||||||||
# Validate endpoint configuration | ||||||||||||||||||
# Validate endpoint configuration with additional security checks | ||||||||||||||||||
# | ||||||||||||||||||
# @param config [Hash] Endpoint configuration to validate | ||||||||||||||||||
# @return [Hash] Validated configuration | ||||||||||||||||||
|
@@ -72,7 +72,15 @@ def self.validate_endpoint_config(config) | |||||||||||||||||
raise ValidationError, "Invalid endpoint configuration: #{result.errors.to_h}" | ||||||||||||||||||
end | ||||||||||||||||||
|
||||||||||||||||||
result.to_h | ||||||||||||||||||
validated_config = result.to_h | ||||||||||||||||||
|
||||||||||||||||||
# Security: Additional validation for handler name | ||||||||||||||||||
handler_name = validated_config[:handler] | ||||||||||||||||||
unless valid_handler_name?(handler_name) | ||||||||||||||||||
raise ValidationError, "Invalid handler name: #{handler_name}" | ||||||||||||||||||
end | ||||||||||||||||||
|
||||||||||||||||||
validated_config | ||||||||||||||||||
end | ||||||||||||||||||
|
||||||||||||||||||
# Validate array of endpoint configurations | ||||||||||||||||||
|
@@ -93,6 +101,34 @@ def self.validate_endpoints(endpoints) | |||||||||||||||||
|
||||||||||||||||||
validated_endpoints | ||||||||||||||||||
end | ||||||||||||||||||
|
||||||||||||||||||
private | ||||||||||||||||||
|
||||||||||||||||||
# Validate that a handler name is safe | ||||||||||||||||||
# | ||||||||||||||||||
# @param handler_name [String] The handler name to validate | ||||||||||||||||||
# @return [Boolean] true if the handler name is safe, false otherwise | ||||||||||||||||||
def self.valid_handler_name?(handler_name) | ||||||||||||||||||
# Must be a string | ||||||||||||||||||
return false unless handler_name.is_a?(String) | ||||||||||||||||||
|
||||||||||||||||||
# Must not be empty or only whitespace | ||||||||||||||||||
return false if handler_name.strip.empty? | ||||||||||||||||||
|
||||||||||||||||||
# Must match a safe pattern: alphanumeric + underscore, starting with uppercase | ||||||||||||||||||
return false unless handler_name.match?(/\A[A-Z][a-zA-Z0-9_]*\z/) | ||||||||||||||||||
|
||||||||||||||||||
# Must not be a system/built-in class name | ||||||||||||||||||
dangerous_classes = %w[ | ||||||||||||||||||
File Dir Kernel Object Class Module Proc Method | ||||||||||||||||||
IO Socket TCPSocket UDPSocket BasicSocket | ||||||||||||||||||
Process Thread Fiber Mutex ConditionVariable | ||||||||||||||||||
Marshal YAML JSON Pathname | ||||||||||||||||||
] | ||||||||||||||||||
return false if dangerous_classes.include?(handler_name) | ||||||||||||||||||
Comment on lines
+122
to
+128
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [nitpick] This
Suggested change
Copilot uses AI. Check for mistakes. Positive FeedbackNegative Feedback |
||||||||||||||||||
|
||||||||||||||||||
true | ||||||||||||||||||
end | ||||||||||||||||||
end | ||||||||||||||||||
end | ||||||||||||||||||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] Client-side configuration errors (e.g., missing or invalid auth config) are more appropriately represented with a 400 series status code. Consider using 400 instead of 500 for these checks.
Copilot uses AI. Check for mistakes.