Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -659,6 +659,27 @@ protected Set<String> updateEntitiesInRepo(ApplicationGitReference applicationGi
return validPages;
}

/**
* Validates that the given target path, after normalization, is still contained within
* the configured Git root directory. This is a defense-in-depth measure to prevent
* path traversal attacks where crafted resource names could cause file writes outside
* the repository.
*
* @param targetPath the resolved path intended for a file write
* @throws AppsmithPluginException if the path escapes the Git root directory
*/
private void validatePathIsWithinGitRoot(Path targetPath) {
Path normalizedTarget = targetPath.toAbsolutePath().normalize();
Path gitRoot =
Paths.get(gitServiceConfig.getGitRootPath()).toAbsolutePath().normalize();
if (!normalizedTarget.startsWith(gitRoot)) {
String errorMessage = "SECURITY: Path traversal detected. Attempted to write to " + normalizedTarget
+ " which is outside the Git root " + gitRoot;
log.error(errorMessage);
throw new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, errorMessage);
}
}
Comment on lines +671 to +681
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid leaking internal paths in the thrown exception message.

The error message embedded in AppsmithPluginException includes the full normalized target path and the Git root path. If this exception propagates to an API response, it exposes internal directory structure to the caller. Log the detailed message, but throw with a generic one.

Proposed fix
     private void validatePathIsWithinGitRoot(Path targetPath) {
         Path normalizedTarget = targetPath.toAbsolutePath().normalize();
         Path gitRoot =
                 Paths.get(gitServiceConfig.getGitRootPath()).toAbsolutePath().normalize();
         if (!normalizedTarget.startsWith(gitRoot)) {
-            String errorMessage = "SECURITY: Path traversal detected. Attempted to write to " + normalizedTarget
-                    + " which is outside the Git root " + gitRoot;
-            log.error(errorMessage);
-            throw new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, errorMessage);
+            log.error("SECURITY: Path traversal detected. Attempted to write to {} which is outside the Git root {}", normalizedTarget, gitRoot);
+            throw new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, "Path traversal detected: write target is outside the permitted directory");
         }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private void validatePathIsWithinGitRoot(Path targetPath) {
Path normalizedTarget = targetPath.toAbsolutePath().normalize();
Path gitRoot =
Paths.get(gitServiceConfig.getGitRootPath()).toAbsolutePath().normalize();
if (!normalizedTarget.startsWith(gitRoot)) {
String errorMessage = "SECURITY: Path traversal detected. Attempted to write to " + normalizedTarget
+ " which is outside the Git root " + gitRoot;
log.error(errorMessage);
throw new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, errorMessage);
}
}
private void validatePathIsWithinGitRoot(Path targetPath) {
Path normalizedTarget = targetPath.toAbsolutePath().normalize();
Path gitRoot =
Paths.get(gitServiceConfig.getGitRootPath()).toAbsolutePath().normalize();
if (!normalizedTarget.startsWith(gitRoot)) {
log.error("SECURITY: Path traversal detected. Attempted to write to {} which is outside the Git root {}", normalizedTarget, gitRoot);
throw new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, "Path traversal detected: write target is outside the permitted directory");
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/server/appsmith-git/src/main/java/com/appsmith/git/files/FileUtilsCEImpl.java`
around lines 671 - 681, In validatePathIsWithinGitRoot, keep logging the
detailed error (including normalizedTarget and gitRoot) via log.error, but
change the thrown AppsmithPluginException to use a generic error message (e.g.,
"SECURITY: Path traversal detected") so internal paths are not leaked; ensure
you still use AppsmithPluginError.PLUGIN_ERROR when constructing the exception
and do not remove the detailed log call for diagnostics.


/**
* This method will be used to store the DB resource to JSON file
*
Expand All @@ -667,6 +688,7 @@ protected Set<String> updateEntitiesInRepo(ApplicationGitReference applicationGi
* @return if the file operation is successful
*/
protected boolean saveResource(Object sourceEntity, Path path) {
validatePathIsWithinGitRoot(path);
try {
Files.createDirectories(path.getParent());
return fileOperations.writeToFile(sourceEntity, path);
Expand All @@ -678,6 +700,7 @@ protected boolean saveResource(Object sourceEntity, Path path) {
}

protected void saveResourceCommon(Object sourceEntity, Path path) {
validatePathIsWithinGitRoot(path);
try {
Files.createDirectories(path.getParent());
if (sourceEntity instanceof String s) {
Expand Down Expand Up @@ -707,6 +730,7 @@ protected void saveResourceCommon(Object sourceEntity, Path path) {
*/
private boolean saveActionCollection(Object sourceEntity, String body, String resourceName, Path path) {
Span span = observationHelper.createSpan(GitSpan.FILE_WRITE);
validatePathIsWithinGitRoot(path);
Comment on lines 731 to +733
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Span is created before the validation check — minor resource waste on rejection.

observationHelper.createSpan(GitSpan.FILE_WRITE) on Line 732 allocates a tracing span before the path is validated. If the path is rejected, the span is never ended (the finally block on Line 750 would be skipped since the exception is thrown before try). Consider moving validatePathIsWithinGitRoot before createSpan, or wrapping both in the try/finally.

Proposed fix
     private boolean saveActionCollection(Object sourceEntity, String body, String resourceName, Path path) {
-        Span span = observationHelper.createSpan(GitSpan.FILE_WRITE);
         validatePathIsWithinGitRoot(path);
+        Span span = observationHelper.createSpan(GitSpan.FILE_WRITE);
         try {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private boolean saveActionCollection(Object sourceEntity, String body, String resourceName, Path path) {
Span span = observationHelper.createSpan(GitSpan.FILE_WRITE);
validatePathIsWithinGitRoot(path);
private boolean saveActionCollection(Object sourceEntity, String body, String resourceName, Path path) {
validatePathIsWithinGitRoot(path);
Span span = observationHelper.createSpan(GitSpan.FILE_WRITE);
try {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/server/appsmith-git/src/main/java/com/appsmith/git/files/FileUtilsCEImpl.java`
around lines 731 - 733, The span
(observationHelper.createSpan(GitSpan.FILE_WRITE)) is created before the path
validation, so if validatePathIsWithinGitRoot(path) throws the span is never
closed; move the validatePathIsWithinGitRoot(path) call to occur before calling
observationHelper.createSpan(...) inside saveActionCollection, or alternatively
include both the validation and span creation inside the same try block so the
existing finally block always runs to end the span; reference
saveActionCollection, observationHelper.createSpan(GitSpan.FILE_WRITE), and
validatePathIsWithinGitRoot to locate where to reorder or wrap them.

try {
Files.createDirectories(path);
if (StringUtils.hasText(body)) {
Expand Down Expand Up @@ -742,6 +766,7 @@ private boolean saveActionCollection(Object sourceEntity, String body, String re
*/
private boolean saveActions(Object sourceEntity, String body, String resourceName, Path path) {
Span span = observationHelper.createSpan(GitSpan.FILE_WRITE);
validatePathIsWithinGitRoot(path);
Comment on lines 767 to +769
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Same span-before-validation issue in saveActions.

Same as saveActionCollection — the span is created before the validation, and won't be ended if the validation throws.

Proposed fix
     private boolean saveActions(Object sourceEntity, String body, String resourceName, Path path) {
-        Span span = observationHelper.createSpan(GitSpan.FILE_WRITE);
         validatePathIsWithinGitRoot(path);
+        Span span = observationHelper.createSpan(GitSpan.FILE_WRITE);
         try {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/server/appsmith-git/src/main/java/com/appsmith/git/files/FileUtilsCEImpl.java`
around lines 767 - 769, The span in saveActions is created before
validatePathIsWithinGitRoot, so if validation throws the span is never ended;
move the observationHelper.createSpan(GitSpan.FILE_WRITE) call to after
validatePathIsWithinGitRoot(path) in saveActions, and ensure the created Span is
closed/ended in a finally block (matching the pattern used elsewhere) so the
span is always ended even on exceptions.

try {
Files.createDirectories(path);
// Write the user written query to .txt file to make conflict handling easier
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,11 @@ public class ActionCollectionCE_DTO {

public Set<String> validate() {
Set<String> validationErrors = new HashSet<>();
if (this.name == null || this.name.trim().isEmpty()) {
validationErrors.add(AppsmithError.INVALID_PARAMETER.getMessage(FieldName.NAME));
} else if (containsUnsafePathCharacters(this.name)) {
validationErrors.add(AppsmithError.INVALID_PARAMETER.getMessage(FieldName.NAME));
}
if (this.workspaceId == null) {
validationErrors.add(AppsmithError.INVALID_PARAMETER.getMessage(FieldName.WORKSPACE_ID));
}
Expand All @@ -113,6 +118,18 @@ public Set<String> validate() {
return validationErrors;
}

/**
* Checks if the given name contains characters that could be used for path traversal attacks.
* This prevents directory traversal via names like "../../../etc/passwd" when the name is
* used as part of file paths during Git serialization.
*
* @param name the name to check
* @return true if the name contains unsafe path characters
*/
private static boolean containsUnsafePathCharacters(String name) {
return name.contains("..") || name.contains("/") || name.contains("\\") || name.indexOf('\0') >= 0;
}

public void populateTransientFields(ActionCollection actionCollection) {
this.setId(actionCollection.getId());
this.setBaseId(actionCollection.getBaseIdOrFallback());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,7 @@ protected void setArtifactIndependentResources(
if (datasourceList != null) {
datasourceList.forEach(datasource -> {
removeUnwantedFieldsFromDatasource(datasource);
validateGitSerializableName(datasource.getName(), "Datasource");
final String filePath = DATASOURCE_DIRECTORY + DELIMITER_PATH + datasource.getName() + JSON_EXTENSION;
GitResourceIdentity identity =
new GitResourceIdentity(GitResourceType.DATASOURCE_CONFIG, datasource.getGitSyncId(), filePath);
Expand Down Expand Up @@ -369,6 +370,7 @@ protected void setNewActionsInResourceMap(
.forEach(newAction -> {
ActionDTO action = newAction.getUnpublishedAction();
final String actionFileName = action.getUserExecutableName().replace(".", "-");
validateGitSerializableName(actionFileName, "Action");
final String filePathPrefix = getContextDirectoryByType(action.getContextType())
+ DELIMITER_PATH
+ action.calculateContextId()
Expand Down Expand Up @@ -429,6 +431,7 @@ protected void setActionCollectionsInResourceMap(
.forEach(actionCollection -> {
ActionCollectionDTO collection = actionCollection.getUnpublishedCollection();
final String collectionName = collection.getUserExecutableName();
validateGitSerializableName(collectionName, "ActionCollection");
final String filePathPrefix = getContextDirectoryByType(collection.getContextType())
+ DELIMITER_PATH
+ collection.calculateContextId()
Expand Down Expand Up @@ -782,6 +785,27 @@ public Mono<Boolean> checkIfDirectoryIsEmpty(Path baseRepoSuffix) throws IOExcep
.onErrorResume(e -> Mono.error(new AppsmithException(AppsmithError.GIT_FILE_SYSTEM_ERROR, e)));
}

/**
* Validates that a resource name intended for use as a Git file path segment does not contain
* path traversal sequences or directory separator characters. This is a defense-in-depth measure
* that prevents arbitrary file writes even if input validation at the API layer is bypassed.
*
* @param resourceName the name to validate
* @param resourceType a human-readable description of the resource type (for error messages)
* @throws AppsmithException if the name contains path traversal or directory separator characters
*/
protected static void validateGitSerializableName(String resourceName, String resourceType) {
if (resourceName != null
&& (resourceName.contains("..")
|| resourceName.contains("/")
|| resourceName.contains("\\")
|| resourceName.indexOf('\0') >= 0)) {
log.error("SECURITY: Path traversal attempt detected in {} name: '{}'", resourceType, resourceName);
throw new AppsmithException(
AppsmithError.GIT_FILE_SYSTEM_ERROR, "Path traversal detected in " + resourceType + " name");
}
}
Comment on lines +797 to +807
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Potential log injection via resourceName.

Line 803 logs the raw resourceName which may contain newline characters or log format sequences (e.g., %n, ${jndi:...}). While most modern logging frameworks (Logback/Log4j2 with recent patches) mitigate JNDI attacks, consider sanitizing or truncating the name before logging it.

Proposed fix
-            log.error("SECURITY: Path traversal attempt detected in {} name: '{}'", resourceType, resourceName);
+            log.error("SECURITY: Path traversal attempt detected in {} name: '{}'",
+                    resourceType, resourceName.replaceAll("[\\r\\n]", "_"));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
protected static void validateGitSerializableName(String resourceName, String resourceType) {
if (resourceName != null
&& (resourceName.contains("..")
|| resourceName.contains("/")
|| resourceName.contains("\\")
|| resourceName.indexOf('\0') >= 0)) {
log.error("SECURITY: Path traversal attempt detected in {} name: '{}'", resourceType, resourceName);
throw new AppsmithException(
AppsmithError.GIT_FILE_SYSTEM_ERROR, "Path traversal detected in " + resourceType + " name");
}
}
protected static void validateGitSerializableName(String resourceName, String resourceType) {
if (resourceName != null
&& (resourceName.contains("..")
|| resourceName.contains("/")
|| resourceName.contains("\\")
|| resourceName.indexOf('\0') >= 0)) {
log.error("SECURITY: Path traversal attempt detected in {} name: '{}'",
resourceType, resourceName.replaceAll("[\\r\\n]", "_"));
throw new AppsmithException(
AppsmithError.GIT_FILE_SYSTEM_ERROR, "Path traversal detected in " + resourceType + " name");
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/ce/CommonGitFileUtilsCE.java`
around lines 797 - 807, The log call in validateGitSerializableName currently
prints the raw resourceName (in log.error(..., resourceType, resourceName))
which can contain newlines or malicious log sequences; sanitize and truncate
resourceName into a safe value (e.g., remove control characters including \n and
\r, escape/replace percentage/format tokens like '%' and '${', and cap length to
a reasonable max) and use that safeName in the log.error call; keep the
AppsmithException message unchanged (or ensure it does not include raw
resourceName) and update references in validateGitSerializableName to use the
sanitized variable when logging.


public static void removeUnwantedFieldsFromBaseDomain(BaseDomain baseDomain) {
baseDomain.setPolicies(null);
baseDomain.setUserPermissions(null);
Expand Down