Browse Source

Validate Logstash pipeline ID when creating. (#135378) (#135575)

* Validate Logstash pipeline ID when creating.

* Checkstyle issue fix.

* Apply Exford comma.

(cherry picked from commit 9b9e66542d1a52860f71d953dd8598a545fdb42f)
Mashhur 2 weeks ago
parent
commit
9f40e53d83

+ 53 - 0
x-pack/plugin/logstash/src/javaRestTest/java/org/elasticsearch/xpack/test/rest/LogstashSystemIndexIT.java

@@ -150,6 +150,59 @@ public class LogstashSystemIndexIT extends ESRestTestCase {
         assertThat(listResponseMap.size(), is(ids.size()));
     }
 
+    public void testValidPipelineIds() throws IOException {
+        final String pipelineJson = getPipelineJson();
+        final List<String> validIds = List.of(
+            "main",
+            "_internal",
+            "my_pipeline",
+            "my-pipeline",
+            "pipeline123",
+            "A1",
+            "_pipeline_1",
+            "MyPipeline-123",
+            "main_pipeline_v2"
+        );
+
+        for (String id : validIds) {
+            createPipeline(id, pipelineJson);
+        }
+
+        refreshAllIndices();
+
+        // fetch all pipeline IDs
+        Request listAll = new Request("GET", "/_logstash/pipeline");
+        Response listAllResponse = client().performRequest(listAll);
+        assertThat(listAllResponse.getStatusLine().getStatusCode(), is(200));
+        Map<String, Object> listResponseMap = XContentHelper.convertToMap(
+            XContentType.JSON.xContent(),
+            EntityUtils.toString(listAllResponse.getEntity()),
+            false
+        );
+        for (String id : validIds) {
+            assertTrue(listResponseMap.containsKey(id));
+        }
+        assertThat(listResponseMap.size(), is(validIds.size()));
+    }
+
+    public void testInvalidPipelineIds() throws IOException {
+        final String pipelineJson = getPipelineJson();
+        final List<String> invalidPipelineIds = List.of("123pipeline", "-pipeline", "*-pipeline");
+
+        for (String id : invalidPipelineIds) {
+            Request putRequest = new Request("PUT", "/_logstash/pipeline/" + id);
+            putRequest.setJsonEntity(pipelineJson);
+
+            ResponseException exception = expectThrows(ResponseException.class, () -> client().performRequest(putRequest));
+
+            Response response = exception.getResponse();
+            assertThat(response.getStatusLine().getStatusCode(), is(400));
+
+            String responseBody = EntityUtils.toString(response.getEntity());
+            assertThat(responseBody, containsString("Invalid pipeline [" + id + "] ID received"));
+        }
+    }
+
     private void createPipeline(String id, String json) throws IOException {
         Request putRequest = new Request("PUT", "/_logstash/pipeline/" + id);
         putRequest.setJsonEntity(json);

+ 28 - 0
x-pack/plugin/logstash/src/main/java/org/elasticsearch/xpack/logstash/rest/RestPutPipelineAction.java

@@ -8,6 +8,7 @@
 package org.elasticsearch.xpack.logstash.rest;
 
 import org.elasticsearch.client.internal.node.NodeClient;
+import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.bytes.BytesArray;
 import org.elasticsearch.rest.BaseRestHandler;
 import org.elasticsearch.rest.RestRequest;
@@ -23,12 +24,17 @@ import org.elasticsearch.xpack.logstash.action.PutPipelineResponse;
 
 import java.io.IOException;
 import java.util.List;
+import java.util.regex.Pattern;
 
 import static org.elasticsearch.rest.RestRequest.Method.PUT;
 
 @ServerlessScope(Scope.PUBLIC)
 public class RestPutPipelineAction extends BaseRestHandler {
 
+    // A pipeline ID pattern to validate.
+    // Reference: https://www.elastic.co/docs/reference/logstash/configuring-centralized-pipelines#wildcard-in-pipeline-id
+    private static final Pattern PIPELINE_ID_PATTERN = Pattern.compile("[a-zA-Z_][a-zA-Z0-9_-]*");
+
     @Override
     public String getName() {
         return "logstash_put_pipeline";
@@ -39,9 +45,31 @@ public class RestPutPipelineAction extends BaseRestHandler {
         return List.of(new Route(PUT, "/_logstash/pipeline/{id}"));
     }
 
+    /**
+     * Validates pipeline ID for:
+     * - must begin with a letter or underscore
+     * - can contain only letters, underscores, dashes, and numbers
+     */
+    private static void validatePipelineId(String id) {
+        if (Strings.isEmpty(id)) {
+            throw new IllegalArgumentException("Pipeline ID cannot be null or empty");
+        }
+
+        if (PIPELINE_ID_PATTERN.matcher(id).matches() == false) {
+            throw new IllegalArgumentException(
+                "Invalid pipeline ["
+                    + id
+                    + "] ID received. Pipeline ID must begin with a letter or underscore and can contain only letters, "
+                    + "underscores, dashes, hyphens, and numbers"
+            );
+        }
+    }
+
     @Override
     protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException {
         final String id = request.param("id");
+        validatePipelineId(id);
+
         try (XContentParser parser = request.contentParser()) {
             // parse pipeline for validation
             Pipeline.PARSER.apply(parser, id);