diff --git a/lib/mcp/string_utils.rb b/lib/mcp/string_utils.rb index 2dd4e0f..c39c810 100644 --- a/lib/mcp/string_utils.rb +++ b/lib/mcp/string_utils.rb @@ -16,8 +16,8 @@ def demodulize(path) def underscore(camel_cased_word) camel_cased_word - .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2') - .gsub(/([a-z\d])([A-Z])/, '\1_\2') + .gsub(/(?<=[A-Z])(?=[A-Z][a-z])/, "_") + .gsub(/(?<=[a-z\d])(?=[A-Z])/, "_") .tr("-", "_") .downcase end diff --git a/test/mcp/string_utils_test.rb b/test/mcp/string_utils_test.rb index 6613e89..948c34a 100644 --- a/test/mcp/string_utils_test.rb +++ b/test/mcp/string_utils_test.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "test_helper" +require "timeout" module MCP class StringUtilsTest < Minitest::Test @@ -18,5 +19,18 @@ def test_handle_from_class_name_returns_the_class_name_without_the_module_for_a_ assert_equal("test", StringUtils.handle_from_class_name("Module::Submodule::Test")) assert_equal("test_class", StringUtils.handle_from_class_name("Module::Submodule::TestClass")) end + + def test_handle_from_class_name_does_not_cause_redos + # A long string of uppercase letters followed by a non-lowercase character + # would trigger catastrophic backtracking with the vulnerable regex patterns. + malicious_input = "A" * 50_000 + "!" + + result = nil + Timeout.timeout(1) do + result = StringUtils.handle_from_class_name(malicious_input) + end + + assert_equal("a" * 50_000 + "!", result) + end end end