This is an automated email from the ASF dual-hosted git repository.

rmaucher pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tomcat.git

commit 7d31418339cd3c20a1ce92b986cf0c05e3961ba0
Author: opencode <[email protected]>
AuthorDate: Wed Oct 14 11:26:09 2026 +0200

    Recycle C2BConverter in UEncoder.encodeURL to avoid state leaking across 
calls
    
    UEncoder.encodeURL() recycled the internal byte and char chunks but not
    the per-instance C2BConverter. C2BConverter retains characters that
    could not yet be encoded (such as a high surrogate not followed by a low
    surrogate) in a private leftovers buffer. A string ending with such a
    character therefore left it in the converter, and the next encodeURL()
    call on the same instance silently paired the leftover with the start of
    the new, unrelated string, corrupting the result. For example, encoding
    "ab\uD800" followed by "\uDC00cd" produced %f0%90%80%80cd
    (U+10000) instead of %3fcd.
    
    Fix this by calling c2b.recycle() whenever an existing converter is
    reused. C2BConverter.recycle() resets the charset encoder and clears the
    leftovers buffer. Add a regression test that compares the second call's
    result with that of a fresh encoder.
---
 java/org/apache/tomcat/util/buf/UEncoder.java     |  5 +++++
 test/org/apache/tomcat/util/buf/TestUEncoder.java | 22 ++++++++++++++++++++++
 2 files changed, 27 insertions(+)

diff --git a/java/org/apache/tomcat/util/buf/UEncoder.java 
b/java/org/apache/tomcat/util/buf/UEncoder.java
index ec9051b20f..038b9b2ddd 100644
--- a/java/org/apache/tomcat/util/buf/UEncoder.java
+++ b/java/org/apache/tomcat/util/buf/UEncoder.java
@@ -93,6 +93,11 @@ public final class UEncoder {
             bb.recycle();
             cb.recycle();
             output.recycle();
+            // Recycle the converter as well. Otherwise, an unpaired surrogate
+            // left over from the previous call is retained in the converter's
+            // internal buffer and silently combined with the start of the
+            // next, unrelated string.
+            c2b.recycle();
         }
 
         for (int i = start; i < end; i++) {
diff --git a/test/org/apache/tomcat/util/buf/TestUEncoder.java 
b/test/org/apache/tomcat/util/buf/TestUEncoder.java
index 73f3b13f38..d72510e68e 100644
--- a/test/org/apache/tomcat/util/buf/TestUEncoder.java
+++ b/test/org/apache/tomcat/util/buf/TestUEncoder.java
@@ -42,4 +42,26 @@ public class TestUEncoder {
         Assert.assertTrue(urlEncoder.encodeURL(s, 0, s.length())
                 .equals("%f0%90%90%81"));
     }
+
+
+    @Test
+    public void testEncodeURLUnpairedSurrogateDoesNotLeak() throws IOException 
{
+        UEncoder urlEncoder = new UEncoder(SafeCharsSet.DEFAULT);
+
+        // A string ending with an unpaired high surrogate. The surrogate
+        // cannot be encoded. It must not be retained by the encoder and
+        // combined with the start of a later, unrelated string.
+        String loneHigh = "ab" + '\uD800';
+        urlEncoder.encodeURL(loneHigh, 0, loneHigh.length());
+
+        String low = '\uDC00' + "cd";
+        String result = urlEncoder.encodeURL(low, 0, low.length()).toString();
+
+        // The result must match that of a fresh encoder, i.e. the unpaired
+        // low surrogate must be replaced, not paired with the leftover high
+        // surrogate from the previous call
+        UEncoder fresh = new UEncoder(SafeCharsSet.DEFAULT);
+        Assert.assertEquals(fresh.encodeURL(low, 0, low.length()).toString(),
+                result);
+    }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to