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;
+ }
+}