From 5c97a0a78be4ed69ff100049255a1c129e4a8a44 Mon Sep 17 00:00:00 2001 From: youdie006 Date: Thu, 10 Sep 2026 13:47:07 +0900 Subject: [PATCH] Decode on a binary copy in the pure-Ruby unescape unescape called String#tr on the caller's still-tagged string and only converted to binary on the next line, so it raised on bytes that are invalid in that encoding: CGI.unescape("\x80&".dup.force_encoding("UTF-8")) # ArgumentError: invalid byte sequence in UTF-8 The C extension returns the string unchanged, and the three siblings in this file already take a binary copy first: escape and escapeURIComponent take string.b before transforming, and escape even defers its own tr! until after the conversion. unescape was the only one that transformed first. Take the binary copy first and use tr! on it, matching escape. This affects the truffleruby and jruby rows, which both resolve CGI.unescape to this file; the CRuby rows use the C extension and already behave this way. --- lib/cgi/escape.rb | 4 ++-- test/cgi/test_cgi_escape.rb | 12 ++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/lib/cgi/escape.rb b/lib/cgi/escape.rb index bdcdcdf..2fe773c 100644 --- a/lib/cgi/escape.rb +++ b/lib/cgi/escape.rb @@ -33,8 +33,8 @@ def escape(string) # # => "'Stop!' said Fred" def unescape(string, encoding = @@accept_charset) string = string_value(string) - str = string.tr('+', ' ') - str = str.b + str = string.b + str.tr!('+', ' ') str.gsub!(/((?:%[0-9a-fA-F]{2})+)/) do |m| [m.delete('%')].pack('H*') end diff --git a/test/cgi/test_cgi_escape.rb b/test/cgi/test_cgi_escape.rb index 3278bde..93a2e00 100644 --- a/test/cgi/test_cgi_escape.rb +++ b/test/cgi/test_cgi_escape.rb @@ -83,6 +83,18 @@ def test_cgi_unescape_nil assert_raise(TypeError) { CGI.unescape(nil) } end + def test_cgi_unescape_invalid_byte_sequence + # unescape must not raise on bytes that are invalid in the string's own + # encoding; escape and unescapeURIComponent already decode on a binary copy. + s = "\x80&".dup.force_encoding("UTF-8") + assert_equal(s.b, CGI.unescape(s.dup).b) + assert_equal(s.b, CGI.unescapeURIComponent(s.dup).b) + # ordinary decoding is unchanged + assert_equal("a b", CGI.unescape("a+b")) + assert_equal("+", CGI.unescape("%2B")) + assert_equal("'Stop!' said Fred", CGI.unescape("%27Stop%21%27+said+Fred")) + end + def test_cgi_escapeURIComponent assert_equal('%26%3C%3E%22%20%E3%82%86%E3%82%93%E3%82%86%E3%82%93', CGI.escapeURIComponent(@str1)) assert_equal('%26%3C%3E%22%20%E3%82%86%E3%82%93%E3%82%86%E3%82%93'.ascii_only?, CGI.escapeURIComponent(@str1).ascii_only?) if defined?(::Encoding)