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

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


The following commit(s) were added to refs/heads/main by this push:
     new f6872652966 SOLR-18345: ClientUtils.encodeLocalParamVal fix for \, ', 
" (#4729)
f6872652966 is described below

commit f6872652966b1d8b52c376f4a08cd21f622123ab
Author: David Smiley <[email protected]>
AuthorDate: Wed Aug 19 13:22:37 2026 -0400

    SOLR-18345: ClientUtils.encodeLocalParamVal fix for \, ', " (#4729)
    
    SolrJ ClientUtils.encodeLocalParamVal() can produce lossy/invalid encodings 
with a backslash or leading quotes.
    Affects faceting with a custom facet response key.
    Affects the SQL module for LIKE queries.
---
 .../unreleased/SOLR-18345-encodeLocalParamVal.yml  | 10 +++++
 .../apache/solr/client/solrj/util/ClientUtils.java | 21 ++++++++--
 .../solr/client/solrj/util/ClientUtilsTest.java    | 48 +++++++++++++++++++++-
 3 files changed, 74 insertions(+), 5 deletions(-)

diff --git a/changelog/unreleased/SOLR-18345-encodeLocalParamVal.yml 
b/changelog/unreleased/SOLR-18345-encodeLocalParamVal.yml
new file mode 100644
index 00000000000..f176e84b675
--- /dev/null
+++ b/changelog/unreleased/SOLR-18345-encodeLocalParamVal.yml
@@ -0,0 +1,10 @@
+title: >
+  SolrJ ClientUtils.encodeLocalParamVal() can produce lossy/invalid encodings 
with a backslash or leading quotes.
+  Affects faceting with a custom facet response key.
+  Affects the SQL module for LIKE queries.
+type: fixed
+authors:
+  - name: David Smiley
+links:
+  - name: SOLR-18345
+    url: https://issues.apache.org/jira/browse/SOLR-18345
diff --git 
a/solr/solrj/src/java/org/apache/solr/client/solrj/util/ClientUtils.java 
b/solr/solrj/src/java/org/apache/solr/client/solrj/util/ClientUtils.java
index bf06c491d03..32d8006ccca 100644
--- a/solr/solrj/src/java/org/apache/solr/client/solrj/util/ClientUtils.java
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/util/ClientUtils.java
@@ -236,8 +236,19 @@ public class ClientUtils {
     int len = val.length();
     if (0 == len) return "''"; // quoted empty string
 
+    // Note: QueryParsing#parseLocalParams's peek() (used to check for a '=' 
or the closing quote
+    // char) skips leading whitespace as a side effect, so an unquoted empty 
value would silently
+    // absorb the whitespace meant to separate it from the next local param, 
corrupting parsing of
+    // everything that follows. Quoting sidesteps this entirely.
+
     int i = 0;
-    if (len > 0 && val.charAt(0) != '$') {
+    char first = val.charAt(0);
+    // A leading '$' would be read back as a param dereference, and a leading 
quote char would be
+    // read back as the start of a quoted string (StrParser#getQuotedString 
accepts both ' and "
+    // as delimiters); both must be quoted regardless of the rest of the value.
+    if (first == '$' || first == '\'' || first == '"') {
+      // leave i == 0 so the quoting branch below is taken
+    } else {
       for (; i < len; i++) {
         char ch = val.charAt(i);
         if (Character.isWhitespace(ch) || ch == '}') break;
@@ -246,12 +257,16 @@ public class ClientUtils {
 
     if (i >= len) return val;
 
-    // We need to enclose in quotes... but now we need to escape
+    // We need to enclose in quotes... but now we need to escape.  Both the 
quote delimiter itself
+    // and a literal backslash must be escaped: StrParser#getQuotedString 
treats any '\' as the
+    // start of an escape sequence when reading a quoted value, so an 
un-escaped '\' here would be
+    // silently consumed (or worse, combined with the following char into an 
unintended escape like
+    // \n) when the value is parsed back.
     StringBuilder sb = new StringBuilder(val.length() + 4);
     sb.append('\'');
     for (i = 0; i < len; i++) {
       char ch = val.charAt(i);
-      if (ch == '\'') {
+      if (ch == '\'' || ch == '\\') {
         sb.append('\\');
       }
       sb.append(ch);
diff --git 
a/solr/solrj/src/test/org/apache/solr/client/solrj/util/ClientUtilsTest.java 
b/solr/solrj/src/test/org/apache/solr/client/solrj/util/ClientUtilsTest.java
index 2b17ed41386..da6083b108e 100644
--- a/solr/solrj/src/test/org/apache/solr/client/solrj/util/ClientUtilsTest.java
+++ b/solr/solrj/src/test/org/apache/solr/client/solrj/util/ClientUtilsTest.java
@@ -16,12 +16,14 @@
  */
 package org.apache.solr.client.solrj.util;
 
+import org.apache.lucene.tests.util.TestUtil;
 import org.apache.solr.SolrTestCase;
 import org.apache.solr.client.solrj.request.CollectionAdminRequest;
 import org.apache.solr.client.solrj.request.HealthCheckRequest;
 import org.apache.solr.client.solrj.request.QueryRequest;
 import org.apache.solr.client.solrj.request.UpdateRequest;
-import org.apache.solr.client.solrj.request.XMLRequestWriter;
+import org.apache.solr.common.params.ModifiableSolrParams;
+import org.apache.solr.search.QueryParsing;
 import org.junit.Test;
 
 /**
@@ -37,6 +39,49 @@ public class ClientUtilsTest extends SolrTestCase {
     assertEquals("h\\~\\!", ClientUtils.escapeQueryChars("h~!"));
   }
 
+  // FYI also tested via 
org.apache.solr.common.params.SolrParamTest.testLocalParamRoundTripParsing
+  public void testEncodeLocalParamValRoundTrip() throws Exception {
+    // Values that require quoting (whitespace, '}', or a leading '$') must 
round-trip through
+    // Solr's own local-params reader, in particular values containing a 
literal backslash or
+    // single quote.
+    assertRoundTrips("");
+    assertRoundTrips("'leadingQuote");
+    assertRoundTrips("\"leadingDoubleQuote");
+    assertRoundTrips("plain");
+    assertRoundTrips("has space");
+    assertRoundTrips("trailing}brace");
+    assertRoundTrips("has'quote and space");
+    assertRoundTrips("has\\backslash and space");
+    assertRoundTrips("both\\'kinds together");
+    assertRoundTrips("$dollarPrefixed");
+    assertRoundTrips("$dollarPrefixed with space");
+    assertRoundTrips("$\\'mix of everything");
+
+    for (int i = 0; i < 100; i++) {
+      assertRoundTrips(TestUtil.randomUnicodeString(random()));
+    }
+  }
+
+  private void assertRoundTrips(String original) throws Exception {
+    String encoded = ClientUtils.encodeLocalParamVal(original);
+    String txt = "{!key=" + encoded + "}";
+    ModifiableSolrParams target = new ModifiableSolrParams();
+    QueryParsing.parseLocalParams(txt, 0, target, null);
+    assertEquals(
+        "encodeLocalParamVal(" + original + ") -> " + encoded + " did not 
round-trip",
+        original,
+        target.get("key"));
+
+    // Also confirm the encoded value doesn't swallow whatever follows it.
+    String txtFollowedByAnother = "{!key=" + encoded + " next=followed}";
+    ModifiableSolrParams targetFollowedByAnother = new ModifiableSolrParams();
+    QueryParsing.parseLocalParams(txtFollowedByAnother, 0, 
targetFollowedByAnother, null);
+    assertEquals(
+        "encodeLocalParamVal(" + original + ") -> " + encoded + " swallowed 
the next local param",
+        "followed",
+        targetFollowedByAnother.get("next"));
+  }
+
   @Test
   public void testDeterminesWhenToUseDefaultCollection() {
     final var noDefaultNeededRequest = new CollectionAdminRequest.List();
@@ -55,7 +100,6 @@ public class ClientUtilsTest extends SolrTestCase {
 
   @Test
   public void testUrlBuilding() throws Exception {
-    final var rw = new XMLRequestWriter();
     // Simple case, non-collection request
     {
       final var request = new HealthCheckRequest();

Reply via email to