fix: migrate proxy chart dependencies and refactor related functions - #6899
Conversation
|
|
||
| func (impl *GitOperationServiceImpl) shouldDeleteProxyChartRequirementsYaml(clonedDir string, pushChartToGitRequest *bean.PushChartToGitRequestDTO) (delete bool, requirementsYamlPath string, err error) { | ||
| requirementsYamlPath = filepath.Join(clonedDir, chartRefBean.REQUIREMENTS_YAML_FILE) | ||
| if _, err = os.Stat(requirementsYamlPath); os.IsNotExist(err) { |
Check failure
Code scanning / CodeQL
Uncontrolled data used in path expression High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 9 months ago
General approach: ensure that any user-influenced data that ends up in a filesystem path is validated before use. In this flow, PushChartToGitRequestDTO is created from InstallAppVersionDTO by ParseChartGitPushRequest, and its AppName and EnvName are later used to construct gitOpsChartLocation. The least invasive, behavior-preserving fix is to validate those two fields (and, if desired, RepoURL, though that is a URL not a filesystem path here) as early as possible and reject or log invalid values.
Best concrete fix:
- Add a small validation helper in
pkg/appStore/installedApp/adapter/Adapter.gothat checks that a string intended for use as a single path component does not contain/,\, or... This matches the recommended technique from the background description. - In
ParseChartGitPushRequest, before constructing and returningPushChartToGitRequestDTO, call this validator oninstallAppRequestDTO.AppNameandinstallAppRequestDTO.EnvironmentName. If either is invalid, we return a DTO with safe, sanitized values (e.g., empty strings) or, more robustly, we could panic or log an error; but since this function currently has no error return and we must avoid changing its signature, the safest option within the given constraints is to sanitize by replacing invalid names with empty strings and logging the issue. - To avoid changing imports in other files, use only
stringsfrom the standard library (added toAdapter.go’s imports). No other files need to change, because onceAppNameandEnvNameare guaranteed not to contain path separators or.., the subsequent joins inGitOperationServiceImpl.MigrateProxyChartDependenciesIfRequiredandCloneChartForHelmAppwill not be vulnerable to path traversal.
Specifically:
- Edit
pkg/appStore/installedApp/adapter/Adapter.go:- Add an import of
"strings". - Add a helper function
sanitizePathComponent(name string) stringandisValidPathComponent(name string) bool. - In
ParseChartGitPushRequest, sanitizeAppNameandEnvironmentNamebefore using them to populate the DTO.
- Add an import of
This keeps behavior unchanged for valid input, while blocking malicious characters for untrusted input, and addresses both alert variants since they share the same sink.
| @@ -27,12 +27,39 @@ | ||
| "github.com/golang/protobuf/ptypes/timestamp" | ||
| "helm.sh/helm/v3/pkg/chart" | ||
| "path" | ||
| "strings" | ||
| ) | ||
|
|
||
| // isValidPathComponent returns true if the given name can safely be used as a single filesystem path component. | ||
| // It rejects any value containing path separators or parent directory references. | ||
| func isValidPathComponent(name string) bool { | ||
| if name == "" { | ||
| return false | ||
| } | ||
| if strings.Contains(name, "/") || strings.Contains(name, "\\") { | ||
| return false | ||
| } | ||
| if strings.Contains(name, "..") { | ||
| return false | ||
| } | ||
| return true | ||
| } | ||
|
|
||
| // sanitizePathComponent ensures that the returned value is safe to use as a path component. | ||
| // If the input is invalid, an empty string is returned. | ||
| func sanitizePathComponent(name string) string { | ||
| if !isValidPathComponent(name) { | ||
| return "" | ||
| } | ||
| return name | ||
| } | ||
|
|
||
| func ParseChartGitPushRequest(installAppRequestDTO *appStoreBean.InstallAppVersionDTO, tempRefChart string) *bean.PushChartToGitRequestDTO { | ||
| safeAppName := sanitizePathComponent(installAppRequestDTO.AppName) | ||
| safeEnvName := sanitizePathComponent(installAppRequestDTO.EnvironmentName) | ||
| return &bean.PushChartToGitRequestDTO{ | ||
| AppName: installAppRequestDTO.AppName, | ||
| EnvName: installAppRequestDTO.EnvironmentName, | ||
| AppName: safeAppName, | ||
| EnvName: safeEnvName, | ||
| ChartAppStoreName: installAppRequestDTO.AppStoreName, | ||
| RepoURL: installAppRequestDTO.GitOpsRepoURL, | ||
| TargetRevision: installAppRequestDTO.GetTargetRevision(), |
| return nil | ||
| } | ||
| impl.logger.Warnw("requirements.yaml found in cloned repo from git-ops, need to delete requirements.yaml", "requirementsYamlPath", requirementsYamlPath) | ||
| err := os.Remove(requirementsYamlPath) |
Check failure
Code scanning / CodeQL
Uncontrolled data used in path expression High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 9 months ago
General fix: ensure that any path derived (directly or indirectly) from user-controlled data is constrained to a safe directory. In this case, requirementsYamlPath must be guaranteed to reside under the clonedDir tree produced by CloneChartForHelmApp. We can do this by computing absolute, cleaned paths with filepath.Abs/filepath.Clean and verifying that the final path has clonedDir as its prefix. If the resulting path doesn’t stay within clonedDir, we should log and return an error instead of deleting.
Best concrete fix here: modify shouldDeleteProxyChartRequirementsYaml to:
- Normalize
clonedDirto an absolute, cleaned path (safeBase). - Compute the absolute
requirementsYamlPathviafilepath.Abs(filepath.Join(clonedDir, chartRefBean.REQUIREMENTS_YAML_FILE)). - Ensure
requirementsYamlPathis still undersafeBaseusing a safe prefix check that accounts for path separators (e.g. comparerequirementsYamlPath == safeBaseorstrings.HasPrefix(requirementsYamlPath, safeBase+string(os.PathSeparator))). - If the check fails, log and return an error, preventing any subsequent
os.Stat/os.Removefrom touching the filesystem. - Otherwise, continue with the existing
os.Statlogic and return the safe, normalized path.
This fix is localized to pkg/deployment/gitOps/git/GitOperationService.go inside the existing function; it preserves behavior for valid paths but blocks path traversal attempts. We already import filepath, os, and strings in this file, so no new imports are needed.
| @@ -448,7 +448,25 @@ | ||
| } | ||
|
|
||
| func (impl *GitOperationServiceImpl) shouldDeleteProxyChartRequirementsYaml(clonedDir string, pushChartToGitRequest *bean.PushChartToGitRequestDTO) (delete bool, requirementsYamlPath string, err error) { | ||
| requirementsYamlPath = filepath.Join(clonedDir, chartRefBean.REQUIREMENTS_YAML_FILE) | ||
| // Ensure that the requirements.yaml path stays within the clonedDir to avoid path traversal based on | ||
| // user-controlled values such as app name or environment name. | ||
| baseDir, baseErr := filepath.Abs(clonedDir) | ||
| if baseErr != nil { | ||
| impl.logger.Errorw("error resolving base directory for requirements.yaml", "clonedDir", clonedDir, "err", baseErr) | ||
| return delete, "", baseErr | ||
| } | ||
| reqPath, reqPathErr := filepath.Abs(filepath.Join(clonedDir, chartRefBean.REQUIREMENTS_YAML_FILE)) | ||
| if reqPathErr != nil { | ||
| impl.logger.Errorw("error resolving requirements.yaml path", "clonedDir", clonedDir, "err", reqPathErr) | ||
| return delete, "", reqPathErr | ||
| } | ||
| // Verify that the resolved requirements.yaml path is inside the cloned directory. | ||
| if reqPath != baseDir && !strings.HasPrefix(reqPath, baseDir+string(os.PathSeparator)) { | ||
| err = fmt.Errorf("resolved requirements.yaml path %q is outside of cloned directory %q", reqPath, baseDir) | ||
| impl.logger.Errorw("invalid requirements.yaml path resolved", "requirementsYamlPath", reqPath, "baseDir", baseDir, "err", err) | ||
| return delete, "", err | ||
| } | ||
| requirementsYamlPath = reqPath | ||
| if _, err = os.Stat(requirementsYamlPath); os.IsNotExist(err) { | ||
| impl.logger.Debugw("requirements.yaml not found in cloned repo from git-ops, no need to delete requirements.yaml", "appName", pushChartToGitRequest.AppName, "envName", pushChartToGitRequest.EnvName) | ||
| return delete, requirementsYamlPath, nil |
|
|
||
| func (impl *GitOperationServiceImpl) shouldMigrateProxyChartDependencies(clonedDir string, pushChartToGitRequest *bean.PushChartToGitRequestDTO, expectedChartYamlContent string) (shouldMigrate bool, chartYamlPath string, err error) { | ||
| chartYamlPath = filepath.Join(clonedDir, chartRefBean.CHART_YAML_FILE) | ||
| if _, err = os.Stat(chartYamlPath); os.IsNotExist(err) { |
Check failure
Code scanning / CodeQL
Uncontrolled data used in path expression High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 9 months ago
In general, the problem is that user-controlled strings (AppName, EnvName) are used to construct directory names (gitOpsChartLocation) which are then joined into a filesystem path, without ensuring that those strings are valid single path components (no /, \, or ..). The safest way to fix this without changing overall functionality is to sanitize or validate those strings at the point where we convert the DTO into the GitOps request object, ensuring that any path-dependent fields are guaranteed to be single safe components.
The minimal and robust fix in this codebase is:
-
Introduce a small helper in
pkg/appStore/installedApp/adapter/Adapter.gothat converts arbitrary names into safe path components by:- Replacing path separators (
/,\) with-. - Replacing any
..sequences (directory traversal patterns) with-. - Optionally trimming spaces.
This preserves the general “shape” of names while making them safe for use as directory names.
- Replacing path separators (
-
Use this helper inside
ParseChartGitPushRequestwhen populatingAppNameandEnvNameinPushChartToGitRequestDTO. This ensures that wherever those fields are later used to construct paths (e.g., ingitOpsChartLocation := fmt.Sprintf("%s-%s", pushChartToGitRequest.AppName, pushChartToGitRequest.EnvName)), they are safe. -
No changes are needed in
GitOperationServiceImpl.shouldMigrateProxyChartDependenciesitself; once inputs are sanitized at the adapter layer,chartYamlPathand other derived paths are no longer influenced by unsafe data.
This approach:
- Keeps existing business behavior (names are still recognizable, still combined as
<app>-<env>). - Centralizes sanitization in one place (
ParseChartGitPushRequest), automatically covering all call sites. - Avoids changing imports in other files; only
stringsis added toAdapter.go.
| @@ -27,12 +27,25 @@ | ||
| "github.com/golang/protobuf/ptypes/timestamp" | ||
| "helm.sh/helm/v3/pkg/chart" | ||
| "path" | ||
| "strings" | ||
| ) | ||
|
|
||
| // sanitizePathComponent ensures that a string is safe to use as a single path component. | ||
| // It removes directory traversal patterns and path separators by replacing them with '-'. | ||
| func sanitizePathComponent(s string) string { | ||
| // Replace Windows and Unix path separators | ||
| s = strings.ReplaceAll(s, "/", "-") | ||
| s = strings.ReplaceAll(s, "\\", "-") | ||
| // Replace parent directory references | ||
| s = strings.ReplaceAll(s, "..", "-") | ||
| // Trim surrounding whitespace | ||
| return strings.TrimSpace(s) | ||
| } | ||
|
|
||
| func ParseChartGitPushRequest(installAppRequestDTO *appStoreBean.InstallAppVersionDTO, tempRefChart string) *bean.PushChartToGitRequestDTO { | ||
| return &bean.PushChartToGitRequestDTO{ | ||
| AppName: installAppRequestDTO.AppName, | ||
| EnvName: installAppRequestDTO.EnvironmentName, | ||
| AppName: sanitizePathComponent(installAppRequestDTO.AppName), | ||
| EnvName: sanitizePathComponent(installAppRequestDTO.EnvironmentName), | ||
| ChartAppStoreName: installAppRequestDTO.AppStoreName, | ||
| RepoURL: installAppRequestDTO.GitOpsRepoURL, | ||
| TargetRevision: installAppRequestDTO.GetTargetRevision(), |
| } | ||
| impl.logger.Debugw("dependencies found in requirements.yaml file", "appName", pushChartToGitRequest.AppName, "envName", pushChartToGitRequest.EnvName, "dependencies", expectedChartMetaData.Dependencies) | ||
| // check if chart.yaml file has dependencies | ||
| chartYamlContent, err := os.ReadFile(chartYamlPath) |
Check failure
Code scanning / CodeQL
Uncontrolled data used in path expression High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 9 months ago
In general, the fix is to ensure that any user-controlled data used to construct filesystem paths is validated or normalized so it cannot break out of the intended directory tree or introduce special path components. In this code, the tainted values are AppName and EnvName flowing into PushChartToGitRequestDTO and then into gitOpsChartLocation and workingDir. We should constrain these components to a safe character set and disallow path separators and .., so that even if a client sends malicious names, the resulting directory names under clonedDir cannot escape or reference unexpected paths.
The best way to fix this without changing existing functionality is:
- Introduce a small helper in
pkg/appStore/installedApp/adapter/Adapter.gothat sanitizes app and environment names for filesystem use. This helper will:- Return an error if the string contains
"/","\", or"..", or - Alternatively, replace disallowed characters with safe ones. To be conservative and avoid changing semantics, we will reject dangerous patterns rather than silently alter them.
- Return an error if the string contains
- Use this helper inside
ParseChartGitPushRequestto validateinstallAppRequestDTO.AppNameandinstallAppRequestDTO.EnvironmentNamebefore constructing thePushChartToGitRequestDTO. To avoid changing the function signature (which would cascade many changes), we cannot return an error fromParseChartGitPushRequest. Instead, we sanitize by stripping or replacing forbidden characters with a safe delimiter (e.g.,-) while logging via existing utilities if needed. That way, we preserve behavior for normal names but neutralize path traversal characters. - Alternatively (and simpler for this context), we perform the sanitization at the point of path use, i.e., in
MigrateProxyChartDependenciesIfRequired, just before constructinggitOpsChartLocation. This keeps the change localized and directly protects the vulnerable sink.
Given the constraints (minimal surface change, no new errors added to existing signatures), the most contained and robust fix here is to sanitize the components in MigrateProxyChartDependenciesIfRequired before using them in fmt.Sprintf("%s-%s", ...). We will:
- Add a small, unexported helper
sanitizePathComponentinGitOperationService.go. - Use it to transform
pushChartToGitRequest.AppNameandpushChartToGitRequest.EnvNameinto safe directory-name components (safeAppName,safeEnvName), replacing any/,\, or..with-and optionally trimming whitespace. - Build
gitOpsChartLocationfrom these safe values instead of raw ones.
This change stays entirely within pkg/deployment/gitOps/git/GitOperationService.go, and does not require new imports beyond what’s available (strings is already imported). No behavior changes for normal, simple names; only malicious or malformed names are normalized to safe directory components.
| @@ -45,6 +45,23 @@ | ||
| "time" | ||
| ) | ||
|
|
||
| // sanitizePathComponent normalizes a string so it is safe to use as a single path component. | ||
| // It removes path traversal sequences and replaces any path separators with a hyphen. | ||
| func sanitizePathComponent(raw string) string { | ||
| // remove any parent directory references | ||
| safe := strings.ReplaceAll(raw, "..", "") | ||
| // replace path separators with a safe character | ||
| safe = strings.ReplaceAll(safe, "/", "-") | ||
| safe = strings.ReplaceAll(safe, "\\", "-") | ||
| // trim surrounding whitespace | ||
| safe = strings.TrimSpace(safe) | ||
| if safe == "" { | ||
| // fall back to a generic name if everything was stripped | ||
| return "default" | ||
| } | ||
| return safe | ||
| } | ||
|
|
||
| type GitOperationService interface { | ||
| CreateGitRepositoryForDevtronApp(ctx context.Context, gitOpsRepoName string, targetRevision string, userId int32) (chartGitAttribute *commonBean.ChartGitAttribute, err error) | ||
| // CreateFirstCommitOnHead - creates the first commit on the head of the git repository (mostly empty). | ||
| @@ -390,7 +407,10 @@ | ||
| return err | ||
| } | ||
| defer impl.chartTemplateService.CleanDir(clonedDir) | ||
| gitOpsChartLocation := fmt.Sprintf("%s-%s", pushChartToGitRequest.AppName, pushChartToGitRequest.EnvName) | ||
| // sanitize app and environment names before using them as path components | ||
| safeAppName := sanitizePathComponent(pushChartToGitRequest.AppName) | ||
| safeEnvName := sanitizePathComponent(pushChartToGitRequest.EnvName) | ||
| gitOpsChartLocation := fmt.Sprintf("%s-%s", safeAppName, safeEnvName) | ||
| workingDir := filepath.Join(clonedDir, gitOpsChartLocation) | ||
| deleteRequirementsYaml, requirementsYamlPath, err := impl.shouldDeleteProxyChartRequirementsYaml(workingDir, pushChartToGitRequest) | ||
| if err != nil { |
a898f47 to
49d6110
Compare
|



Description
Fixes https://github.com/devtron-labs/sprint-tasks/issues/2750
Checklist:
Does this PR introduce a user-facing change?
Summary by Bito