1
0
Эх сурвалжийг харах

HLRC: Fix '+' Not Correctly Encoded in GET Req. (#33164)

* HLRC: Fix '+' Not Correctly Encoded in GET Req.

* Encode `+` correctly as `%2B` in URL paths
* Keep encoding `+` as space in URL parameters
* Closes #33077
Armin Braun 6 жил өмнө
parent
commit
fe2a870668

+ 18 - 0
client/rest-high-level/src/test/java/org/elasticsearch/client/CrudIT.java

@@ -1020,6 +1020,24 @@ public class CrudIT extends ESRestHighLevelClientTestCase {
         }
     }
 
+    public void testGetIdWithPlusSign() throws Exception {
+        String id = "id+id";
+        {
+            IndexRequest indexRequest = new IndexRequest("index").id(id);
+            indexRequest.source("field", "value");
+            IndexResponse indexResponse = highLevelClient().index(indexRequest, RequestOptions.DEFAULT);
+            assertEquals("index", indexResponse.getIndex());
+            assertEquals(id, indexResponse.getId());
+        }
+        {
+            GetRequest getRequest = new GetRequest("index").id(id);
+            GetResponse getResponse = highLevelClient().get(getRequest, RequestOptions.DEFAULT);
+            assertTrue(getResponse.isExists());
+            assertEquals("index", getResponse.getIndex());
+            assertEquals(id, getResponse.getId());
+        }
+    }
+
     // Not entirely sure if _termvectors belongs to CRUD, and in the absence of a better place, will have it here
     public void testTermvectors() throws IOException {
         final String sourceIndex = "index1";

+ 9 - 1
docs/reference/migration/migrate_8_0/http.asciidoc

@@ -12,4 +12,12 @@
 ==== Removal of old HTTP settings
 
 The `http.tcp_no_delay` setting was deprecated in 7.x and has been removed in 8.0. It has been replaced by
-`http.tcp.no_delay`.
+`http.tcp.no_delay`.
+
+[float]
+==== Changes to Encoding Plus Signs in URLs
+
+Starting in version 7.3, a `+` in a URL will be encoded as `%2B` by all REST API functionality. Prior versions handled a `+` as a single space.
+If your application requires handling `+` as a single space you can return to the old behaviour by setting the system property
+`es.rest.url_plus_as_space` to `true`. Note that this behaviour is deprecated and setting this system property to `true` will cease
+to be supported in version 8.

+ 32 - 24
server/src/main/java/org/elasticsearch/rest/RestUtils.java

@@ -19,6 +19,7 @@
 
 package org.elasticsearch.rest;
 
+import org.elasticsearch.common.Booleans;
 import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.path.PathTrie;
 
@@ -30,6 +31,11 @@ import java.util.regex.Pattern;
 
 public class RestUtils {
 
+    /**
+     * Sets whether we decode a '+' in an url as a space or not.
+     */
+    private static final boolean DECODE_PLUS_AS_SPACE = Booleans.parseBoolean(System.getProperty("es.rest.url_plus_as_space", "false"));
+
     public static final PathTrie.Decoder REST_DECODER = new PathTrie.Decoder() {
         @Override
         public String decode(String value) {
@@ -55,7 +61,7 @@ public class RestUtils {
             c = s.charAt(i);
             if (c == '=' && name == null) {
                 if (pos != i) {
-                    name = decodeComponent(s.substring(pos, i));
+                    name = decodeQueryStringParam(s.substring(pos, i));
                 }
                 pos = i + 1;
             } else if (c == '&' || c == ';') {
@@ -63,9 +69,9 @@ public class RestUtils {
                     // We haven't seen an `=' so far but moved forward.
                     // Must be a param of the form '&a&' so add it with
                     // an empty value.
-                    addParam(params, decodeComponent(s.substring(pos, i)), "");
+                    addParam(params, decodeQueryStringParam(s.substring(pos, i)), "");
                 } else if (name != null) {
-                    addParam(params, name, decodeComponent(s.substring(pos, i)));
+                    addParam(params, name, decodeQueryStringParam(s.substring(pos, i)));
                     name = null;
                 }
                 pos = i + 1;
@@ -74,15 +80,19 @@ public class RestUtils {
 
         if (pos != i) {  // Are there characters we haven't dealt with?
             if (name == null) {     // Yes and we haven't seen any `='.
-                addParam(params, decodeComponent(s.substring(pos, i)), "");
+                addParam(params, decodeQueryStringParam(s.substring(pos, i)), "");
             } else {                // Yes and this must be the last value.
-                addParam(params, name, decodeComponent(s.substring(pos, i)));
+                addParam(params, name, decodeQueryStringParam(s.substring(pos, i)));
             }
         } else if (name != null) {  // Have we seen a name without value?
             addParam(params, name, "");
         }
     }
 
+    private static String decodeQueryStringParam(final String s) {
+        return decodeComponent(s, StandardCharsets.UTF_8, true);
+    }
+
     private static void addParam(Map<String, String> params, String name, String value) {
         params.put(name, value);
     }
@@ -90,7 +100,7 @@ public class RestUtils {
     /**
      * Decodes a bit of an URL encoded by a browser.
      * <p>
-     * This is equivalent to calling {@link #decodeComponent(String, Charset)}
+     * This is equivalent to calling {@link #decodeComponent(String, Charset, boolean)}
      * with the UTF-8 charset (recommended to comply with RFC 3986, Section 2).
      *
      * @param s The string to decode (can be empty).
@@ -100,7 +110,7 @@ public class RestUtils {
      *                                  escape sequence.
      */
     public static String decodeComponent(final String s) {
-        return decodeComponent(s, StandardCharsets.UTF_8);
+        return decodeComponent(s, StandardCharsets.UTF_8, DECODE_PLUS_AS_SPACE);
     }
 
     /**
@@ -119,52 +129,50 @@ public class RestUtils {
      * Actually this function doesn't allocate any memory if there's nothing
      * to decode, the argument itself is returned.
      *
-     * @param s       The string to decode (can be empty).
-     * @param charset The charset to use to decode the string (should really
-     *                be {@link StandardCharsets#UTF_8}.
+     * @param s           The string to decode (can be empty).
+     * @param charset     The charset to use to decode the string (should really
+     *                    be {@link StandardCharsets#UTF_8}.
+     * @param plusAsSpace Whether to decode a {@code '+'} to a single space {@code ' '}
      * @return The decoded string, or {@code s} if there's nothing to decode.
      *         If the string to decode is {@code null}, returns an empty string.
      * @throws IllegalArgumentException if the string contains a malformed
      *                                  escape sequence.
      */
-    public static String decodeComponent(final String s, final Charset charset) {
+    private static String decodeComponent(final String s, final Charset charset, boolean plusAsSpace) {
         if (s == null) {
             return "";
         }
         final int size = s.length();
-        if (!decodingNeeded(s, size)) {
+        if (!decodingNeeded(s, size, plusAsSpace)) {
             return s;
         }
         final byte[] buf = new byte[size];
-        int pos = decode(s, size, buf);
+        int pos = decode(s, size, buf, plusAsSpace);
         return new String(buf, 0, pos, charset);
     }
 
-    @SuppressWarnings("fallthrough")
-    private static boolean decodingNeeded(String s, int size) {
+    private static boolean decodingNeeded(String s, int size, boolean plusAsSpace) {
         boolean decodingNeeded = false;
         for (int i = 0; i < size; i++) {
             final char c = s.charAt(i);
-            switch (c) {
-                case '%':
-                    i++;  // We can skip at least one char, e.g. `%%'.
-                    // Fall through.
-                case '+':
-                    decodingNeeded = true;
-                    break;
+            if (c == '%') {
+                i++;  // We can skip at least one char, e.g. `%%'.
+                decodingNeeded = true;
+            } else if (plusAsSpace && c == '+') {
+                decodingNeeded = true;
             }
         }
         return decodingNeeded;
     }
 
     @SuppressWarnings("fallthrough")
-    private static int decode(String s, int size, byte[] buf) {
+    private static int decode(String s, int size, byte[] buf, boolean plusAsSpace) {
         int pos = 0;  // position in `buf'.
         for (int i = 0; i < size; i++) {
             char c = s.charAt(i);
             switch (c) {
                 case '+':
-                    buf[pos++] = ' ';  // "+" -> " "
+                    buf[pos++] = (byte) (plusAsSpace ? ' ' : '+');  // "+" -> " "
                     break;
                 case '%':
                     if (i == size - 1) {

+ 2 - 2
x-pack/plugin/ml/qa/basic-multi-node/src/test/java/org/elasticsearch/xpack/ml/integration/MlBasicMultiNodeIT.java

@@ -14,9 +14,9 @@ import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.xcontent.XContentBuilder;
 import org.elasticsearch.test.rest.ESRestTestCase;
 import org.elasticsearch.xpack.ml.MachineLearning;
+import org.yaml.snakeyaml.util.UriEncoder;
 
 import java.io.IOException;
-import java.net.URLEncoder;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
@@ -303,7 +303,7 @@ public class MlBasicMultiNodeIT extends ESRestTestCase {
         }
         xContentBuilder.endObject();
 
-        Request request = new Request("PUT", MachineLearning.BASE_PATH + "anomaly_detectors/" + URLEncoder.encode(jobId, "UTF-8"));
+        Request request = new Request("PUT", MachineLearning.BASE_PATH + "anomaly_detectors/" + UriEncoder.encode(jobId));
         request.setJsonEntity(Strings.toString(xContentBuilder));
         return client().performRequest(request);
     }