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

mgubaidullin pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel-karavan.git


The following commit(s) were added to refs/heads/main by this push:
     new 5e425249 PathUtils
5e425249 is described below

commit 5e4252494817af0cd2697216bd02f361037fedcf
Author: Marat Gubaidullin <[email protected]>
AuthorDate: Fri Aug 28 08:37:52 2026 -0400

    PathUtils
---
 .../camel/karavan/api/ProjectFileResource.java     | 26 +++++++-
 .../apache/camel/karavan/service/CodeService.java  |  6 +-
 .../apache/camel/karavan/service/GitService.java   | 10 ++-
 .../org/apache/camel/karavan/util/PathUtils.java   | 77 ++++++++++++++++++++++
 4 files changed, 114 insertions(+), 5 deletions(-)

diff --git 
a/karavan-app/src/main/java/org/apache/camel/karavan/api/ProjectFileResource.java
 
b/karavan-app/src/main/java/org/apache/camel/karavan/api/ProjectFileResource.java
index a8d5ea5a..43b91ed7 100644
--- 
a/karavan-app/src/main/java/org/apache/camel/karavan/api/ProjectFileResource.java
+++ 
b/karavan-app/src/main/java/org/apache/camel/karavan/api/ProjectFileResource.java
@@ -25,6 +25,7 @@ import jakarta.ws.rs.core.Response;
 import org.apache.camel.karavan.cache.KaravanCache;
 import org.apache.camel.karavan.cache.ProjectFile;
 import org.apache.camel.karavan.cache.ProjectFileCommited;
+import org.apache.camel.karavan.util.PathUtils;
 
 import java.net.URLDecoder;
 import java.nio.charset.StandardCharsets;
@@ -115,6 +116,12 @@ public class ProjectFileResource {
     @Produces(MediaType.APPLICATION_JSON)
     @Consumes(MediaType.APPLICATION_JSON)
     public Response create(ProjectFile file) throws Exception {
+        try {
+            PathUtils.validateFileName(file.getName());
+            PathUtils.validateProjectId(file.getProjectId());
+        } catch (IllegalArgumentException e) {
+            return 
Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
+        }
         file.setLastUpdate(Instant.now().getEpochSecond() * 1000L);
         boolean projectFileExists = 
karavanCache.getProjectFile(file.getProjectId(), file.getName()) != null;
         if (projectFileExists) {
@@ -129,10 +136,16 @@ public class ProjectFileResource {
     @Authenticated
     @Produces(MediaType.APPLICATION_JSON)
     @Consumes(MediaType.APPLICATION_JSON)
-    public ProjectFile update(ProjectFile file) throws Exception {
+    public Response update(ProjectFile file) throws Exception {
+        try {
+            PathUtils.validateFileName(file.getName());
+            PathUtils.validateProjectId(file.getProjectId());
+        } catch (IllegalArgumentException e) {
+            return 
Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
+        }
         file.setLastUpdate(Instant.now().getEpochSecond() * 1000L);
         karavanCache.saveProjectFile(file, null, true);
-        return file;
+        return Response.ok(file).build();
     }
 
     @PATCH
@@ -145,6 +158,7 @@ public class ProjectFileResource {
                            JsonObject copy) throws Exception {
         try {
             var newName = copy.getString("newName");
+            PathUtils.validateFileName(newName);
             var fromFile = karavanCache.getProjectFile(projectId, filename);
             var toFile = karavanCache.getProjectFile(projectId, newName);
             if (toFile != null) {
@@ -161,6 +175,8 @@ public class ProjectFileResource {
                 karavanCache.deleteProjectFileCommited(projectId, filename);
                 return Response.ok(file).build();
             }
+        } catch (IllegalArgumentException e) {
+            return 
Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
         } catch (Exception e) {
             return Response.serverError().entity(e.getMessage()).build();
         }
@@ -188,6 +204,12 @@ public class ProjectFileResource {
         var toProjectId = copy.getString("toProjectId");
         var toFilename = copy.getString("toFilename");
         var overwrite = copy.getBoolean("overwrite", false);
+        try {
+            PathUtils.validateFileName(toFilename);
+            PathUtils.validateProjectId(toProjectId);
+        } catch (IllegalArgumentException e) {
+            return 
Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
+        }
         var tofile = karavanCache.getProjectFile(toProjectId, toFilename);
         if (overwrite || tofile == null) {
             var file = karavanCache.getProjectFile(fromProjectId, 
fromFilename);
diff --git 
a/karavan-app/src/main/java/org/apache/camel/karavan/service/CodeService.java 
b/karavan-app/src/main/java/org/apache/camel/karavan/service/CodeService.java
index 749cba9b..bf6495ac 100644
--- 
a/karavan-app/src/main/java/org/apache/camel/karavan/service/CodeService.java
+++ 
b/karavan-app/src/main/java/org/apache/camel/karavan/service/CodeService.java
@@ -30,6 +30,7 @@ import org.apache.camel.karavan.cache.ProjectFolder;
 import org.apache.camel.karavan.docker.DockerComposeConverter;
 import org.apache.camel.karavan.model.DockerComposeService;
 import org.apache.camel.karavan.model.PathCommitDetails;
+import org.apache.camel.karavan.util.PathUtils;
 import org.apache.commons.text.StringSubstitutor;
 import org.apache.commons.text.lookup.StringLookup;
 import org.eclipse.microprofile.config.ConfigProvider;
@@ -196,8 +197,11 @@ public class CodeService {
 
     private void addFile(String temp, String fileName, String code) {
         try {
-            String path = temp + File.separator + fileName;
+            String path = PathUtils.resolveInside(Paths.get(temp), 
fileName).toString();
             vertx.fileSystem().writeFileBlocking(path, Buffer.buffer(code));
+        } catch (SecurityException e) {
+            LOGGER.error("Path traversal blocked for file " + fileName);
+            throw e;
         } catch (Exception e) {
             LOGGER.error(e.getMessage());
         }
diff --git 
a/karavan-app/src/main/java/org/apache/camel/karavan/service/GitService.java 
b/karavan-app/src/main/java/org/apache/camel/karavan/service/GitService.java
index 9bd6c409..c2d73d0b 100644
--- a/karavan-app/src/main/java/org/apache/camel/karavan/service/GitService.java
+++ b/karavan-app/src/main/java/org/apache/camel/karavan/service/GitService.java
@@ -28,6 +28,7 @@ import org.apache.camel.karavan.cache.ProjectFile;
 import org.apache.camel.karavan.cache.ProjectFolder;
 import org.apache.camel.karavan.model.GitConfig;
 import org.apache.camel.karavan.model.PathCommitDetails;
+import org.apache.camel.karavan.util.PathUtils;
 import org.eclipse.jgit.api.*;
 import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.api.errors.InvalidRemoteException;
@@ -298,12 +299,17 @@ public class GitService {
     }
 
     private void writeProjectToFolder(String folder, ProjectFolder 
projectFolder, List<ProjectFile> files) throws IOException {
-        Files.createDirectories(Paths.get(folder, 
projectFolder.getProjectId()));
+        Path projectDir = PathUtils.resolveInside(Paths.get(folder), 
projectFolder.getProjectId());
+        Files.createDirectories(projectDir);
         LOGGER.info("Write files for project " + projectFolder.getProjectId());
         files.forEach(file -> {
             try {
+                Path target = PathUtils.resolveInside(projectDir, 
file.getName());
                 LOGGER.info("Add file " + file.getName());
-                Files.writeString(Paths.get(folder, 
projectFolder.getProjectId(), file.getName()), file.getCode());
+                Files.writeString(target, file.getCode());
+            } catch (SecurityException e) {
+                LOGGER.error("Path traversal blocked for file " + 
file.getName() + " in project " + projectFolder.getProjectId());
+                throw e;
             } catch (IOException e) {
                 LOGGER.error("Error during file write", e);
             }
diff --git 
a/karavan-app/src/main/java/org/apache/camel/karavan/util/PathUtils.java 
b/karavan-app/src/main/java/org/apache/camel/karavan/util/PathUtils.java
new file mode 100644
index 00000000..4558bb4d
--- /dev/null
+++ b/karavan-app/src/main/java/org/apache/camel/karavan/util/PathUtils.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.karavan.util;
+
+import java.nio.file.InvalidPathException;
+import java.nio.file.Path;
+import java.util.regex.Pattern;
+
+/**
+ * Validation helpers for user supplied names (project ids and file names) 
that end up
+ * as path segments on the file system.
+ */
+public final class PathUtils {
+
+    private static final Pattern SAFE_NAME_PATTERN = 
Pattern.compile("^[a-zA-Z0-9_\\-.]+$");
+
+    private PathUtils() {
+    }
+
+    /**
+     * Checks that a single path segment (file name or project id) is safe to 
use on the file system.
+     *
+     * @throws IllegalArgumentException if the name is empty or may escape its 
parent directory
+     */
+    public static void validateName(String kind, String name) {
+        if (name == null || name.isBlank()) {
+            throw new IllegalArgumentException(kind + " cannot be empty");
+        }
+        if (name.contains("..") || name.contains("/") || name.contains("\\") 
|| name.indexOf('\0') >= 0) {
+            throw new IllegalArgumentException(kind + " contains path 
traversal characters: " + name);
+        }
+        if (!SAFE_NAME_PATTERN.matcher(name).matches()) {
+            throw new IllegalArgumentException(kind + " contains invalid 
characters: " + name);
+        }
+    }
+
+    public static void validateFileName(String fileName) {
+        validateName("Filename", fileName);
+    }
+
+    public static void validateProjectId(String projectId) {
+        validateName("Project id", projectId);
+    }
+
+    /**
+     * Resolves a name against a base directory and guarantees that the result 
stays inside it.
+     *
+     * @throws SecurityException if the resolved path escapes the base 
directory
+     */
+    public static Path resolveInside(Path baseDir, String name) {
+        Path base = baseDir.toAbsolutePath().normalize();
+        Path target;
+        try {
+            target = base.resolve(name).toAbsolutePath().normalize();
+        } catch (InvalidPathException e) {
+            throw new SecurityException("Invalid path: " + name, e);
+        }
+        if (!target.startsWith(base) || target.equals(base)) {
+            throw new SecurityException("Path traversal detected in: " + name);
+        }
+        return target;
+    }
+}

Reply via email to