Skip to content
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

Add validation to helm values file path to prevent potential dirtrav vulnerability #3726

Merged
merged 21 commits into from
Jun 7, 2022
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 @@ -314,7 +314,7 @@ One of `yamlField` or `regex` is required.
| Field | Type | Description | Required |
|-|-|-|-|
| releaseName | string | The release name of helm deployment. By default, the release name is equal to the application name. | No |
| valueFiles | []string | List of value files should be loaded. | No |
| valueFiles | []string | List of value files should be loaded. Only local files stored under the application directory or remote files served at the http(s) endpoint are allowed. | No |
| setFiles | map[string]string | List of file path for values. | No |
| apiVersions | []string | Kubernetes api versions used for Capabilities.APIVersions. | No |
| kubeVersion | string | Kubernetes version used for Capabilities.KubeVersion. | No |
Expand Down
74 changes: 73 additions & 1 deletion pkg/app/piped/cloudprovider/kubernetes/helm.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"bytes"
"context"
"fmt"
"net/url"
"os"
"os/exec"
"path/filepath"
Expand All @@ -30,6 +31,10 @@ import (
"github.com/pipe-cd/pipecd/pkg/config"
)

var (
allowedURLSchemes = []string{"http", "https"}
)

type Helm struct {
version string
execPath string
Expand Down Expand Up @@ -63,6 +68,10 @@ func (c *Helm) TemplateLocalChart(ctx context.Context, appName, appDir, namespac

if opts != nil {
for _, v := range opts.ValueFiles {
if err := verifyHelmValueFilePath(appDir, v); err != nil {
c.logger.Error("failed to verify values file path", zap.Error(err))
return "", err
}
args = append(args, "-f", v)
}
for k, v := range opts.SetFiles {
Expand Down Expand Up @@ -99,7 +108,7 @@ type helmRemoteGitChart struct {
}

func (c *Helm) TemplateRemoteGitChart(ctx context.Context, appName, appDir, namespace string, chart helmRemoteGitChart, gitClient gitClient, opts *config.InputHelmOptions) (string, error) {
// Firstly, we need to download the remote repositoy.
// Firstly, we need to download the remote repository.
nghialv marked this conversation as resolved.
Show resolved Hide resolved
repoDir, err := os.MkdirTemp("", "helm-remote-chart")
if err != nil {
return "", fmt.Errorf("unable to created temporary directory for storing remote helm chart: %w", err)
Expand Down Expand Up @@ -153,6 +162,10 @@ func (c *Helm) TemplateRemoteChart(ctx context.Context, appName, appDir, namespa

if opts != nil {
for _, v := range opts.ValueFiles {
if err := verifyHelmValueFilePath(appDir, v); err != nil {
c.logger.Error("failed to verify values file path", zap.Error(err))
return "", err
}
args = append(args, "-f", v)
}
for k, v := range opts.SetFiles {
Expand Down Expand Up @@ -199,3 +212,62 @@ func (c *Helm) TemplateRemoteChart(ctx context.Context, appName, appDir, namespa
}
return executor()
}

// verifyHelmValueFilePath verifies if the path of the values file references
// a remote URL or inside the path where the application configuration file (i.e. *.pipecd.yaml) is located.
func verifyHelmValueFilePath(appDir, valueFilePath string) error {
url, err := url.Parse(valueFilePath)
if err == nil && url.Scheme != "" {
for _, s := range allowedURLSchemes {
Copy link
Member

@khanhtc1202 khanhtc1202 Jun 7, 2022

Choose a reason for hiding this comment

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

was it a miss or did we really think about allowing the remote valueFiles? I feel a bit unsafe to allow remote file value, wdyt?

Copy link
Contributor Author

@Szarny Szarny Jun 7, 2022

Choose a reason for hiding this comment

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

Since our concern here is the unauthorized rendering of unrelated sensitive information on the host on which Piped is running, remote files are outside the scope of this PR's concern, I think.

However, there may be such a case: SSRF, i.e., when a server accessible only from a host running Piped holds the confidential values file and attackers tries to render it (e.g. valueFile: https://private-server-that-can-be-accessible-only-from-piped-host.com/values.yaml) illegally.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

To prevent this, it may be necessary to specify an allowlist of URLs from which the values file can be read.
However, I think that allowing reachable HTTP(S) endpoints is an acceptable risk under the current circumstances.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This concept follows the advisory of GHSA-63qx-x74g-jcr7.

Copy link
Member

Choose a reason for hiding this comment

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

I understand that this is out of context on this PR 👍 My concern is currently, do we allow the remote valueFiles that way? and if it's yes, then the docs we updated may need to be updated as well.

Only files stored under the application directory are allowed.

Copy link
Member

Choose a reason for hiding this comment

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

okay, lets go with that option 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

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

updated it 👍

Copy link
Member

Choose a reason for hiding this comment

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

@Szarny
Sorry, I didn't know that ability.
Please let me confirm, will Helm automatically fetches the remote value files to apply when it was specified with HTTP scheme?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@nghialv Yes, please see this pr: helm/helm#2769

Copy link
Member

Choose a reason for hiding this comment

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

Thanks for teaching me. Didn't know about that before. 👍

if strings.EqualFold(url.Scheme, s) {
return nil
}
}

return fmt.Errorf("scheme %s is not allowed to load values file", url.Scheme)
}

// valueFilePath is a path where non-default Helm values file is located.
if !filepath.IsAbs(valueFilePath) {
valueFilePath = filepath.Join(appDir, valueFilePath)
}

if isSymlink(valueFilePath) {
if valueFilePath, err = resolveSymlinkToAbsPath(valueFilePath, appDir); err != nil {
return err
}
}

// If a path outside of appDir is specified as the path for the values file,
// it may indicate that someone trying to illegally read a file as values file that
// exists in the environment where Piped is running.
if !strings.HasPrefix(valueFilePath, appDir) {
return fmt.Errorf("values file %s references outside the application configuration directory", valueFilePath)
}

return nil
}

// isSymlink returns the path is whether symbolic link or not.
func isSymlink(path string) bool {
lstat, err := os.Lstat(path)
if err != nil {
return false
}

return lstat.Mode()&os.ModeSymlink == os.ModeSymlink
}

// resolveSymlinkToAbsPath resolves symbolic link to an absolute path.
func resolveSymlinkToAbsPath(path, absParentDir string) (string, error) {
resolved, err := os.Readlink(path)
if err != nil {
return "", err
}

if !filepath.IsAbs(resolved) {
resolved = filepath.Join(absParentDir, resolved)
}

return resolved, nil
}
89 changes: 89 additions & 0 deletions pkg/app/piped/cloudprovider/kubernetes/helm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,92 @@ func TestTemplateLocalChart_WithNamespace(t *testing.T) {
require.Equal(t, namespace, metadata["namespace"])
}
}

func TestVerifyHelmValueFilePath(t *testing.T) {
t.Parallel()

testcases := []struct {
name string
appDir string
valueFilePath string
wantErr bool
}{
{
name: "Values file locates inside the app dir",
appDir: "testdata/testhelm/appconfdir",
valueFilePath: "values.yaml",
wantErr: false,
},
{
name: "Values file locates inside the app dir (with ..)",
appDir: "testdata/testhelm/appconfdir",
valueFilePath: "../../../testdata/testhelm/appconfdir/values.yaml",
wantErr: false,
},
{
name: "Values file locates under the app dir",
appDir: "testdata/testhelm/appconfdir",
valueFilePath: "dir/values.yaml",
wantErr: false,
},
{
name: "Values file locates under the app dir (with ..)",
appDir: "testdata/testhelm/appconfdir",
valueFilePath: "../../../testdata/testhelm/appconfdir/dir/values.yaml",
wantErr: false,
},
{
name: "arbitrary file locates outside the app dir",
appDir: "testdata/testhelm/appconfdir",
valueFilePath: "/etc/hosts",
wantErr: true,
},
{
name: "arbitrary file locates outside the app dir (with ..)",
appDir: "testdata/testhelm/appconfdir",
valueFilePath: "../../../../../../../../../../../../etc/hosts",
wantErr: true,
},
{
name: "Values file locates allowed remote URL (http)",
appDir: "testdata/testhelm/appconfdir",
valueFilePath: "http://exmaple.com/values.yaml",
wantErr: false,
},
{
name: "Values file locates allowed remote URL (https)",
appDir: "testdata/testhelm/appconfdir",
valueFilePath: "https://exmaple.com/values.yaml",
wantErr: false,
},
{
name: "Values file locates disallowed remote URL (ftp)",
appDir: "testdata/testhelm/appconfdir",
valueFilePath: "ftp://exmaple.com/values.yaml",
wantErr: true,
},
{
name: "Values file is symlink targeting valid values file",
appDir: "testdata/testhelm/appconfdir",
valueFilePath: "valid-symlink",
wantErr: false,
},
{
name: "Values file is symlink targeting invalid values file",
appDir: "testdata/testhelm/appconfdir",
valueFilePath: "invalid-symlink",
wantErr: true,
},
}

for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
err := verifyHelmValueFilePath(tc.appDir, tc.valueFilePath)
if tc.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}
Empty file.