diff --git a/CHANGELOG.md b/CHANGELOG.md index fc376e3..67262e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,8 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Unreleased -Nil. +### Fixed +- Fixed slow matching in `ESCAPED_REGEX` on strings with many unclosed `<`, `{{`, `%{` or `&` (CWE-1333). Lazy wildcards became character classes bounded by each token's own delimiters. Output is unchanged or improved for well-formed input; a token containing a nested opener of its own kind (`x`, `{{ a {b} }}`, `&a&`) is no longer swallowed up to the first closer. Quoted HTML attribute values may now contain `<` and `>`. ## [0.9.1] - 2024-06-27 ### Added diff --git a/lib/pseudolocalization/pseudolocalizer.rb b/lib/pseudolocalization/pseudolocalizer.rb index e7c81ab..648f3ea 100644 --- a/lib/pseudolocalization/pseudolocalizer.rb +++ b/lib/pseudolocalization/pseudolocalizer.rb @@ -3,11 +3,11 @@ module I18n class Pseudolocalizer ESCAPED_REGEX = Regexp.new("(#{ [ - "<.*?>", - "{{.*?}}", - "%{.*?}", + "<[^<>\"'\\n]*(?:(?:\"[^\"\\n]*\"|'[^'\\n]*')[^<>\"'\\n]*)*>", + "{{[^{}\\n]*}}", + "%{[^{}\\n]*}", "https?:\/\/\\S+", - "&\\S*?;" + "&[^\\s&;]*;" ].join('|') })") diff --git a/test/pseudolocalization_test.rb b/test/pseudolocalization_test.rb index 19045c9..8c82105 100644 --- a/test/pseudolocalization_test.rb +++ b/test/pseudolocalization_test.rb @@ -66,6 +66,46 @@ def test_it_works_with_templates assert_equal('Ḥḛḛḽḽṓṓ, ẁṓṓṛḽḍ %{firstname} %{lastname}!', @backend.translate(:en, 'Hello, world %{firstname} %{lastname}!', {})) end + def test_markup_never_spans_a_newline + assert_equal("αα <ṡṗααṇ\nͼḽααṡṡ=\"ẋ\">ḅ", @backend.translate(:en, "a b", {})) + assert_equal("αα {{\n ṇααṃḛḛ }} ḅ", @backend.translate(:en, "a {{\n name }} b", {})) + end + + def test_unterminated_openers_are_pseudolocalized_as_text + assert_equal('αα <ḅ ͼ', @backend.translate(:en, 'a ͼ ḍ', @backend.translate(:en, 'a c d', {})) + assert_equal('αα {{ ḅ | ϝ: {ͼ} }} ḍ', @backend.translate(:en, 'a {{ b | f: {c} }} d', {})) + assert_equal('αα %{ḅ{ͼ} ḍ', @backend.translate(:en, 'a %{b{c} d', {})) + assert_equal('αα &ḅ& ͼ', @backend.translate(:en, 'a &b& c', {})) + end + + def test_quoted_attribute_values_can_contain_angle_brackets + assert_equal('ḽḭḭṇḳ', @backend.translate(:en, 'link', {})) + assert_equal("ḽḭḭṇḳ", @backend.translate(:en, "link", {})) + end + + def test_many_unterminated_openers_complete_in_linear_time + ['<', '<"', "<'", '{{', '%{', '&'].each do |opener| + input = opener * 100_000 + started = Process.clock_gettime(Process::CLOCK_MONOTONIC) + assert_equal(input, @backend.translate(:en, input, {})) + elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started + assert_operator elapsed, :<, 2.0, "#{opener.inspect} * 100_000 took #{elapsed.round(2)}s" + end + end + + def test_escaped_regex_is_linear_time + skip 'Regexp.linear_time? was added in Ruby 3.2' unless Regexp.respond_to?(:linear_time?) + + assert Regexp.linear_time?(Pseudolocalization::I18n::Pseudolocalizer::ESCAPED_REGEX) + end + def test_it_allows_ignoring_cetain_keys @backend.ignores = ['Ignore*', /Clifford.$/]