-
-
Notifications
You must be signed in to change notification settings - Fork 52
feat: support for include compose directive
#817
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
include compose directive
| } | ||
|
|
||
| // 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
user-provided value
Show autofix suggestion
Hide autofix suggestion
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:
-
In
WriteIncludeFile:- Before any call to
os.MkdirAll(dir, ...), resolve symlinks fordirusingfilepath.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.
- Before any call to
-
If any symlink resolution or prefix match fails, abort the operation with an error.
-
Use the already-resolved
absProjectDir(possibly afterEvalSymlinks) 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.
-
Copy modified line R210 -
Copy modified lines R216-R220 -
Copy modified lines R226-R230 -
Copy modified line R232 -
Copy modified lines R238-R239
| @@ -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) | ||
| } | ||
| } |
|
|
||
| // 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
user-provided value
Show autofix suggestion
Hide autofix suggestion
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.Relto 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.MkdirAllso that directory creation is strictly guaranteed to only ever take place insideprojectDirand 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).
-
Copy modified line R210 -
Copy modified lines R221-R227
| @@ -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") | ||
| } | ||
|
|
|
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
user-provided value
Show autofix suggestion
Hide autofix suggestion
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
includePathis 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.
-
Copy modified lines R131-R137
| @@ -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 fix the codeql security alerts |
…#823) Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: kmendell <[email protected]>
|
This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged. |
Closes: #393