Repository: kestra-io/kestra
Issue: [UI] Label containing a URL cannot be used to filter Executions
Labels: bug

### Explain the bug

Screenshare showing the issue https://share.descript.com/view/cBT4aNM66ur 

Reproducer:

```yaml
id: labels
namespace: dev

labels:
  user: anna-geller
  url: "https://github.com/kestra-io/kestra/issues/2632"

tasks:
  - id: log
    type: io.kestra.core.tasks.log.Log
    message: hello there
```

![image](https://github.com/kestra-io/kestra/assets/86264395/fe3e1832-897c-4cf8-b05a-cac5d5cb2c73)


Server stack trace:
```
2023-12-19 13:47:45,140 ERROR io-executor-thread-1 i.k.w.controllers.ErrorController Invalid queryString parameter
io.micronaut.http.exceptions.HttpStatusException: Invalid queryString parameter
	at io.kestra.webserver.utils.RequestUtils.lambda$toMap$0(RequestUtils.java:18)
	at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
	at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1625)
	at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)
	at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
	at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:921)
	at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
	at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:682)
	at io.kestra.webserver.utils.RequestUtils.toMap(RequestUtils.java:26)
	at io.kestra.webserver.controllers.ExecutionController.find(ExecutionController.java:160)
	at io.kestra.webserver.controllers.$ExecutionController$Definition$Intercepted.$$access$$find(Unknown Source)
	at io.kestra.webserver.controllers.$ExecutionController$Definition$Exec.dispatch(Unknown Source)
	at io.micronaut.context.AbstractExecutableMethodsDefinition$DispatchedExecutableMethod.invoke(AbstractExecutableMethodsDefinition.java:371)
	at io.micronaut.aop.chain.MethodInterceptorChain.proceed(MethodInterceptorChain.java:128)
	at io.micronaut.validation.ValidatingInterceptor.intercept(ValidatingInterceptor.java:143)
	at io.micronaut.aop.chain.MethodInterceptorChain.proceed(MethodInterceptorChain.java:137)
	at io.kestra.webserver.controllers.$ExecutionController$Definition$Intercepted.find(Unknown Source)
	at io.kestra.webserver.controllers.$ExecutionController$Definition$Exec.dispatch(Unknown Source)
	at io.micronaut.context.AbstractExecutableMethodsDefinition$DispatchedExecutableMethod.invoke(AbstractExecutableMethodsDefinition.java:371)
	at io.micronaut.context.DefaultBeanContext$4.invoke(DefaultBeanContext.java:594)
	at io.micronaut.web.router.AbstractRouteMatch.execute(AbstractRouteMatch.java:303)
	at io.micronaut.web.router.RouteMatch.execute(RouteMatch.java:111)
	at io.micronaut.http.context.ServerRequestContext.with(ServerRequestContext.java:103)
	at io.micronaut.http.server.RouteExecutor.lambda$executeRoute$14(RouteExecutor.java:659)
	at reactor.core.publisher.FluxDeferContextual.subscribe(FluxDeferContextual.java:49)
	at reactor.core.publisher.InternalFluxOperator.subscribe(InternalFluxOperator.java:62)
	at reactor.core.publisher.FluxSubscribeOn$SubscribeOnSubscriber.run(FluxSubscribeOn.java:194)
	at io.micronaut.reactive.reactor.instrument.ReactorInstrumentation.lambda$init$0(ReactorInstrumentation.java:62)
	at reactor.core.scheduler.WorkerTask.call(WorkerTask.java:84)
	at reactor.core.scheduler.WorkerTask.call(WorkerTask.java:37)
	at io.micrometer.core.instrument.composite.CompositeTimer.recordCallable(CompositeTimer.java:129)
	at io.micrometer.core.instrument.Timer.lambda$wrap$1(Timer.java:206)
	at io.micronaut.scheduling.instrument.InvocationInstrumenterWrappedCallable.call(InvocationInstrumenterWrappedCallable.java:53)
	at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
	at java.base/java.lang.Thread.run(Thread.java:1623)
```

### Environment Information

- Kestra Version: develop
- Operating System and Java Version (if not using Kestra Docker image):


Issue comments:

Pull request review comments:
This is the same as using indexOf(':') and taking what' s before as key and what's after as value right?
By doing that we miss some protection that we have before with miss-crafted query string
```suggestion
    public static final String ENCODED_URL_LABEL_VALUE = URLEncoder.encode(URL_LABEL_VALUE, StandardCharsets.UTF_8));
```

The all string must be encoded
That's what I did before, I forgot a comment but I saw that this is the behaviour of our UI library (for eg. ":" aren't encoded from UI) so I prefer to replicate this encoding to ensure we don't miss any side-effect (":" parsing in this example)
I don't think we should handle the syntax at this level. The goal of this method should only be to extract before & after first ":" imo.
For the indexOf yes I can also do that :+1: 
So in this case indexOf is straightforward
We should URL encode everything from the UI, this is a bug and a security risk I think

Patch that resolved the issue:
diff --git a/webserver/src/main/java/io/kestra/webserver/controllers/ExecutionController.java b/webserver/src/main/java/io/kestra/webserver/controllers/ExecutionController.java
index 166e1962f9..2473280014 100644
--- a/webserver/src/main/java/io/kestra/webserver/controllers/ExecutionController.java
+++ b/webserver/src/main/java/io/kestra/webserver/controllers/ExecutionController.java
@@ -526,14 +526,8 @@ public Execution trigger(
     }
 
     private List<Label> parseLabels(List<String> labels) {
-        return labels == null ? null : labels.stream()
-            .map(label ->  {
-                String[] split = label.split(":");
-                if (split.length != 2) {
-                    throw new HttpStatusException(HttpStatus.UNPROCESSABLE_ENTITY, "Invalid labels parameter");
-                }
-                return new Label(split[0], split[1]);
-            })
+        return labels == null ? null : RequestUtils.toMap(labels).entrySet().stream()
+            .map(entry -> new Label(entry.getKey(), entry.getValue()))
             .toList();
     }
 
diff --git a/webserver/src/main/java/io/kestra/webserver/utils/RequestUtils.java b/webserver/src/main/java/io/kestra/webserver/utils/RequestUtils.java
index 4684206f2d..00843cf661 100644
--- a/webserver/src/main/java/io/kestra/webserver/utils/RequestUtils.java
+++ b/webserver/src/main/java/io/kestra/webserver/utils/RequestUtils.java
@@ -4,6 +4,7 @@
 import io.micronaut.http.exceptions.HttpStatusException;
 
 import java.util.AbstractMap;
+import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
 import java.util.stream.Collectors;
@@ -13,14 +14,14 @@ public static Map<String, String> toMap(List<String> queryString) {
         return queryString == null ? null : queryString
             .stream()
             .map(s -> {
-                String[] split = s.split(":");
-                if (split.length != 2) {
+                String[] split = s.split("[: ]+");
+                if (split.length < 2) {
                     throw new HttpStatusException(HttpStatus.UNPROCESSABLE_ENTITY, "Invalid queryString parameter");
                 }
 
                 return new AbstractMap.SimpleEntry<>(
                     split[0],
-                    split[1]
+                    s.substring(s.indexOf(":") + 1)
                 );
             })
             .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));


Test patch:
diff --git a/webserver/src/test/java/io/kestra/webserver/controllers/ExecutionControllerTest.java b/webserver/src/test/java/io/kestra/webserver/controllers/ExecutionControllerTest.java
index e72356b7ba..6abf893d68 100644
--- a/webserver/src/test/java/io/kestra/webserver/controllers/ExecutionControllerTest.java
+++ b/webserver/src/test/java/io/kestra/webserver/controllers/ExecutionControllerTest.java
@@ -37,12 +37,13 @@
 import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
 
 import java.io.File;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
 import java.time.Duration;
 import java.util.*;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
-import java.util.concurrent.atomic.AtomicReference;
 import java.util.stream.IntStream;
 
 import static io.kestra.core.utils.Rethrow.throwRunnable;
@@ -52,6 +53,8 @@
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 class ExecutionControllerTest extends JdbcH2ControllerTest {
+    public static final String URL_LABEL_VALUE = "https://some-url.com";
+    public static final String ENCODED_URL_LABEL_VALUE = URL_LABEL_VALUE.replace("/", URLEncoder.encode("/", StandardCharsets.UTF_8));
     @Inject
     EmbeddedServer embeddedServer;
 
@@ -97,7 +100,7 @@ void getNotFound() {
     private Execution triggerExecution(String namespace, String flowId, MultipartBody requestBody, Boolean wait) {
         return client.toBlocking().retrieve(
             HttpRequest
-                .POST("/api/v1/executions/trigger/" + namespace + "/" + flowId + "?labels=a:label-1,b:label-2" + (wait ? "&wait=true" : ""), requestBody)
+                .POST("/api/v1/executions/trigger/" + namespace + "/" + flowId + "?labels=a:label-1,b:label-2,url:" + ENCODED_URL_LABEL_VALUE + (wait ? "&wait=true" : ""), requestBody)
                 .contentType(MediaType.MULTIPART_FORM_DATA_TYPE),
             Execution.class
         );
@@ -139,11 +142,12 @@ void trigger() {
         assertThat(result.getInputs().get("file").toString(), startsWith("kestra:///io/kestra/tests/inputs/executions/"));
         assertThat(result.getInputs().containsKey("bool"), is(true));
         assertThat(result.getInputs().get("bool"), nullValue());
-        assertThat(result.getLabels().size(), is(4));
+        assertThat(result.getLabels().size(), is(5));
         assertThat(result.getLabels().get(0), is(new Label("flow-label-1", "flow-label-1")));
         assertThat(result.getLabels().get(1), is(new Label("flow-label-2", "flow-label-2")));
         assertThat(result.getLabels().get(2), is(new Label("a", "label-1")));
         assertThat(result.getLabels().get(3), is(new Label("b", "label-2")));
+        assertThat(result.getLabels().get(4), is(new Label("url", URL_LABEL_VALUE)));
     }
 
     @Test
@@ -669,6 +673,14 @@ void find() {
 
         assertThat(executions.getTotal(), is(0L));
 
+        triggerInputsFlowExecution(false);
+
+        // + is there to simulate that a space was added (this can be the case from UI autocompletion for eg.)
+        executions = client.toBlocking().retrieve(
+            HttpRequest.GET("/api/v1/executions/search?page=1&size=25labels=url:+"+ENCODED_URL_LABEL_VALUE), PagedResults.class
+        );
+
+        assertThat(executions.getTotal(), is(1L));
     }
 
     @Test