Denial of Service / Performance vulnerability (regex-based path in number formatting)
Description
The commit addresses a performance vulnerability in ActiveSupport::NumberHelper::NumberToDelimitedConverter related to formatting large numbers with a delimiter pattern. Previously, the conversion path always invoked a regular expression-based delimited insertion (via delimiter_pattern). This regex-based path can be computationally expensive and susceptible to denial-of-service-like CPU exhaustion on large inputs or crafted patterns. The fix introduces a guarded branch: if a delimiter_pattern is not present, it uses a deterministic manual grouping path (no regex) to insert delimiters; if a delimiter_pattern is present, it continues to use the regex-based approach. The change reduces exposure by avoiding regex-based processing for inputs where a straightforward grouping is possible, and adds tests asserting expected formatted outputs for edge cases (e.g., 1234 -> 1,234; 12345 -> 12,345).
Proof of Concept
# Proof of concept (DoS via large numeric input triggering regex-based path)
# Requires Rails environment with ActiveSupport loaded (Rails 8.1.x vulnerable path)
require 'benchmark'
require 'active_support/all'
# Construct a very large integer (many digits) to stress the formatting path.
# The DoS risk scales with input size due to regex processing.
num_str = '9' * 10000 # 10k digits; increase to stress further if needed
n = num_str.to_i
puts "Formatting a 10k-digit number..."
Benchmark.bm(20) do |x|
x.report("number_to_delimited") do
ActiveSupport::NumberHelper.number_to_delimited(n, delimiter: ',')
end
end
# Expected: the operation consumes significantly more CPU time on vulnerable versions
# compared to fixed versions. In a secure environment, time should remain reasonable.
Commit Details
Author: Jean Boussier
Date: 2026-03-04 09:33 UTC
Message:
Improve performance of NumberToDelimitedConverter
[CVE-2026-33169]
[GHSA-cg4j-q9v8-6v38]
Triage Assessment
Vulnerability Type: Denial of Service / Performance vulnerability related to regex parsing
Confidence: HIGH
Reasoning:
The commit references a CVE and GHSA entry, and the code changes modify the number formatting path to avoid a potential regex-based operation when a delimiter pattern is present. It adds a manual grouping fallback that prevents reliance on a potentially vulnerable regex, addressing performance/security concerns (likely a Denial of Service risk) related to input handling during number formatting.
Verification Assessment
Vulnerability Type: Denial of Service / Performance vulnerability (regex-based path in number formatting)
Confidence: HIGH
Affected Versions: Rails 8.1.0 - 8.1.3 (prior to 8.1.4+)
Code Diff
diff --git a/activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb b/activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb
index 4fb2fb7150fc8..7575429423e03 100644
--- a/activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb
+++ b/activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb
@@ -16,9 +16,24 @@ def convert
private
def parts
left, right = number.to_s.split(".")
- left.gsub!(delimiter_pattern) do |digit_to_delimit|
- "#{digit_to_delimit}#{options[:delimiter]}"
+ if delimiter_pattern
+ left.gsub!(delimiter_pattern) do |digit_to_delimit|
+ "#{digit_to_delimit}#{options[:delimiter]}"
+ end
+ else
+ left_parts = []
+ offset = left.size % 3
+ if offset > 0
+ left_parts << left[0, offset]
+ end
+
+ (left.size / 3).times do |i|
+ left_parts << left[offset + (i * 3), 3]
+ end
+
+ left = left_parts.join(options[:delimiter])
end
+
[left, right].compact
end
diff --git a/activesupport/test/number_helper_test.rb b/activesupport/test/number_helper_test.rb
index bae6a9eb2261e..da57df0ff7ad4 100644
--- a/activesupport/test/number_helper_test.rb
+++ b/activesupport/test/number_helper_test.rb
@@ -139,6 +139,8 @@ def test_to_delimited
assert_equal("12,345,678", number_helper.number_to_delimited(12345678))
assert_equal("0", number_helper.number_to_delimited(0))
assert_equal("123", number_helper.number_to_delimited(123))
+ assert_equal("1,234", number_helper.number_to_delimited(1234))
+ assert_equal("12,345", number_helper.number_to_delimited(12345))
assert_equal("123,456", number_helper.number_to_delimited(123456))
assert_equal("123,456.78", number_helper.number_to_delimited(123456.78))
assert_equal("123,456.789", number_helper.number_to_delimited(123456.789))