Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@kmendell
Copy link
Member

@kmendell kmendell commented Oct 30, 2025

Closes: #393

image

@kmendell kmendell changed the title feat: support for include compose directive feat: support for include compose directive Oct 30, 2025
}

// Only create directory if it doesn't exist
if _, err := os.Stat(dir); os.IsNotExist(err) {

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI 8 days ago

To completely fix this, we should ensure that the directory to be created (dir), along with any file created inside it, is after symlink resolution strictly under the "real" project directory (after symlinks). This entails not only checking absDir and absProjectDir as currently done, but also explicitly evaluating symlinks on the directory (dir) before creating it, just as we've done for the main file path within ValidateIncludePathForWrite.

Steps:

  1. In WriteIncludeFile:

    • Before any call to os.MkdirAll(dir, ...), resolve symlinks for dir using filepath.EvalSymlinks.
    • If resolution fails (directory doesn't exist), then use the cleaned absolute path for the new directory.
    • Then, check that the evaluated directory path, after symlink resolution, starts with (matches) the evaluated absolute project directory path (the same as used to validate write access).
    • Only then proceed to create the directory.
  2. If any symlink resolution or prefix match fails, abort the operation with an error.

  3. Use the already-resolved absProjectDir (possibly after EvalSymlinks) for directory comparison. This provides a stricter check that can't be bypassed by symlinks or odd path manipulations.

No new methods are needed, only an extra symlink evaluation and check in the region that decides if it's safe to mkdir.

Suggested changeset 1
backend/internal/utils/projects/includes.go

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/backend/internal/utils/projects/includes.go b/backend/internal/utils/projects/includes.go
--- a/backend/internal/utils/projects/includes.go
+++ b/backend/internal/utils/projects/includes.go
@@ -207,26 +207,36 @@
 		return fmt.Errorf("invalid include path: cannot create directory '%s'", dir)
 	}
 
-	// Additional check: ensure 'dir' is inside the project directory
+	// Additional check: ensure 'dir' is inside the project directory (after symlink evaluation)
 	absProjectDir, err := filepath.Abs(projectDir)
 	if err != nil {
 		return fmt.Errorf("invalid project directory: %w", err)
 	}
 	absProjectDir = filepath.Clean(absProjectDir)
+	// Evaluate symlinks for project directory if it exists
+	if evalProjectDir, err := filepath.EvalSymlinks(absProjectDir); err == nil {
+		absProjectDir = evalProjectDir
+	}
+
 	absDir, err := filepath.Abs(dir)
 	if err != nil {
 		return fmt.Errorf("invalid include directory: %w", err)
 	}
 	absDir = filepath.Clean(absDir)
+	// Evaluate symlinks for dir if possible
+	evalDir := absDir
+	if evalAbsDir, err := filepath.EvalSymlinks(absDir); err == nil {
+		evalDir = evalAbsDir
+	}
 	projectPrefix := absProjectDir + string(filepath.Separator)
-	isWithinProject := strings.HasPrefix(absDir+string(filepath.Separator), projectPrefix)
+	isWithinProject := strings.HasPrefix(evalDir+string(filepath.Separator), projectPrefix)
 	if !isWithinProject {
 		return fmt.Errorf("write access denied: include directory is outside project directory")
 	}
 
 	// Only create directory if it doesn't exist
-	if _, err := os.Stat(dir); os.IsNotExist(err) {
-		if err := os.MkdirAll(dir, 0755); err != nil {
+	if _, err := os.Stat(evalDir); os.IsNotExist(err) {
+		if err := os.MkdirAll(evalDir, 0755); err != nil {
 			return fmt.Errorf("failed to create directory: %w", err)
 		}
 	}
EOF
@@ -207,26 +207,36 @@
return fmt.Errorf("invalid include path: cannot create directory '%s'", dir)
}

// Additional check: ensure 'dir' is inside the project directory
// Additional check: ensure 'dir' is inside the project directory (after symlink evaluation)
absProjectDir, err := filepath.Abs(projectDir)
if err != nil {
return fmt.Errorf("invalid project directory: %w", err)
}
absProjectDir = filepath.Clean(absProjectDir)
// Evaluate symlinks for project directory if it exists
if evalProjectDir, err := filepath.EvalSymlinks(absProjectDir); err == nil {
absProjectDir = evalProjectDir
}

absDir, err := filepath.Abs(dir)
if err != nil {
return fmt.Errorf("invalid include directory: %w", err)
}
absDir = filepath.Clean(absDir)
// Evaluate symlinks for dir if possible
evalDir := absDir
if evalAbsDir, err := filepath.EvalSymlinks(absDir); err == nil {
evalDir = evalAbsDir
}
projectPrefix := absProjectDir + string(filepath.Separator)
isWithinProject := strings.HasPrefix(absDir+string(filepath.Separator), projectPrefix)
isWithinProject := strings.HasPrefix(evalDir+string(filepath.Separator), projectPrefix)
if !isWithinProject {
return fmt.Errorf("write access denied: include directory is outside project directory")
}

// Only create directory if it doesn't exist
if _, err := os.Stat(dir); os.IsNotExist(err) {
if err := os.MkdirAll(dir, 0755); err != nil {
if _, err := os.Stat(evalDir); os.IsNotExist(err) {
if err := os.MkdirAll(evalDir, 0755); err != nil {
return fmt.Errorf("failed to create directory: %w", err)
}
}
Copilot is powered by AI and may make mistakes. Always verify output.

// Only create directory if it doesn't exist
if _, err := os.Stat(dir); os.IsNotExist(err) {
if err := os.MkdirAll(dir, 0755); err != nil {

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI 8 days ago

The optimal fix is to rework directory containment validation so that it cannot be bypassed by edge-case directory traversal or symlink tricks. This involves:

  • Using filepath.Rel to compute the relative path from the project base to the intended directory and ensuring it does not escape the project directory (i.e., does not start with .. or contain it as a separate element, and is not absolute).
  • Ensure this check occurs after symlinks are resolved and paths are cleaned.
  • Update the check before calling os.MkdirAll so that directory creation is strictly guaranteed to only ever take place inside projectDir and not elsewhere.
  • No additional dependencies are required; Go stdlib suffices.

Edit only the relevant code block in backend/internal/utils/projects/includes.go where the check and directory creation happens (lines around 210-229).

Suggested changeset 1
backend/internal/utils/projects/includes.go

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/backend/internal/utils/projects/includes.go b/backend/internal/utils/projects/includes.go
--- a/backend/internal/utils/projects/includes.go
+++ b/backend/internal/utils/projects/includes.go
@@ -207,7 +207,7 @@
 		return fmt.Errorf("invalid include path: cannot create directory '%s'", dir)
 	}
 
-	// Additional check: ensure 'dir' is inside the project directory
+	// Additional check: ensure 'dir' is inside the project directory using filepath.Rel
 	absProjectDir, err := filepath.Abs(projectDir)
 	if err != nil {
 		return fmt.Errorf("invalid project directory: %w", err)
@@ -218,9 +218,13 @@
 		return fmt.Errorf("invalid include directory: %w", err)
 	}
 	absDir = filepath.Clean(absDir)
-	projectPrefix := absProjectDir + string(filepath.Separator)
-	isWithinProject := strings.HasPrefix(absDir+string(filepath.Separator), projectPrefix)
-	if !isWithinProject {
+
+	relativeDir, err := filepath.Rel(absProjectDir, absDir)
+	if err != nil {
+		return fmt.Errorf("could not compute relative path for include directory: %w", err)
+	}
+	// If the relative path starts with ".." (or is absolute), it's outside the project
+	if relativeDir == "" || strings.HasPrefix(relativeDir, ".."+string(filepath.Separator)) || relativeDir == ".." || filepath.IsAbs(relativeDir) {
 		return fmt.Errorf("write access denied: include directory is outside project directory")
 	}
 
EOF
@@ -207,7 +207,7 @@
return fmt.Errorf("invalid include path: cannot create directory '%s'", dir)
}

// Additional check: ensure 'dir' is inside the project directory
// Additional check: ensure 'dir' is inside the project directory using filepath.Rel
absProjectDir, err := filepath.Abs(projectDir)
if err != nil {
return fmt.Errorf("invalid project directory: %w", err)
@@ -218,9 +218,13 @@
return fmt.Errorf("invalid include directory: %w", err)
}
absDir = filepath.Clean(absDir)
projectPrefix := absProjectDir + string(filepath.Separator)
isWithinProject := strings.HasPrefix(absDir+string(filepath.Separator), projectPrefix)
if !isWithinProject {

relativeDir, err := filepath.Rel(absProjectDir, absDir)
if err != nil {
return fmt.Errorf("could not compute relative path for include directory: %w", err)
}
// If the relative path starts with ".." (or is absolute), it's outside the project
if relativeDir == "" || strings.HasPrefix(relativeDir, ".."+string(filepath.Separator)) || relativeDir == ".." || filepath.IsAbs(relativeDir) {
return fmt.Errorf("write access denied: include directory is outside project directory")
}

Copilot is powered by AI and may make mistakes. Always verify output.
@github-actions
Copy link

This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.

}
}

if err := os.WriteFile(validatedPath, []byte(content), 0600); err != nil {

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI 8 days ago

The best way to fix this issue is to ensure that user input for the include file path (includePath/relativePath) cannot contain path traversal attempts (".."), absolute paths, or path separators. The logic should reject any path that is absolute, contains "..", "/" or "", or would resolve outside the project directory after normalization and symlink resolution.
To implement this in the current context, harden the validation in ValidateIncludePathForWrite as follows:

  • Before joining/basing anything, check that includePath is not absolute, does not contain "..", "/", or "" (thus must be a single file name or valid subdirectory—but you may allow subdirectories if that's a requirement; otherwise, restrict to filenames).
  • Optionally, enforce a regex/whitelist if allowable file names are well specified.
  • Retain the existing directory containment check for defense in depth.

Make these changes in backend/internal/utils/projects/includes.go, within the ValidateIncludePathForWrite function, at the top of the function (after the empty check).

No additional dependencies are needed; use the standard library.


Suggested changeset 1
backend/internal/utils/projects/includes.go

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/backend/internal/utils/projects/includes.go b/backend/internal/utils/projects/includes.go
--- a/backend/internal/utils/projects/includes.go
+++ b/backend/internal/utils/projects/includes.go
@@ -128,6 +128,13 @@
 	if includePath == "" {
 		return "", fmt.Errorf("include path cannot be empty")
 	}
+	// Additional validation: must not be absolute, contain "..", or path separators
+	if filepath.IsAbs(includePath) ||
+		strings.Contains(includePath, "..") ||
+		strings.Contains(includePath, "/") ||
+		strings.Contains(includePath, "\\") {
+		return "", fmt.Errorf("invalid include path: path traversal or separators are not allowed")
+	}
 
 	// Resolve project directory to absolute path and evaluate symlinks
 	absProjectDir, err := filepath.Abs(projectDir)
EOF
@@ -128,6 +128,13 @@
if includePath == "" {
return "", fmt.Errorf("include path cannot be empty")
}
// Additional validation: must not be absolute, contain "..", or path separators
if filepath.IsAbs(includePath) ||
strings.Contains(includePath, "..") ||
strings.Contains(includePath, "/") ||
strings.Contains(includePath, "\\") {
return "", fmt.Errorf("invalid include path: path traversal or separators are not allowed")
}

// Resolve project directory to absolute path and evaluate symlinks
absProjectDir, err := filepath.Abs(projectDir)
Copilot is powered by AI and may make mistakes. Always verify output.
@kmendell
Copy link
Member Author

@copilot fix the codeql security alerts

Copy link
Contributor

Copilot AI commented Oct 31, 2025

@kmendell I've opened a new pull request, #823, to work on those changes. Once the pull request is ready, I'll request review from you.

@github-actions
Copy link

github-actions bot commented Nov 1, 2025

This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

⚡️ Feature: support 'include' and multiple nested compose.yml files

2 participants