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

[Security] Fix graphql server path injection #4809

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
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
66 changes: 66 additions & 0 deletions chaoscenter/graphql/server/pkg/chaoshub/handler/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,11 +155,20 @@ func IsFileExisting(path string) (bool, error) {

// DownloadRemoteHub is used to download a remote hub from the url provided by the user
func DownloadRemoteHub(hubDetails model.CreateRemoteChaosHub, projectID string) error {
if projectID != sanitize.PathName(projectID) {
return fmt.Errorf("err: invalid projectID '%s', potential path injection detected", projectID)
}

dirPath := DefaultPath + projectID
err := os.MkdirAll(dirPath, 0755)
if err != nil {
return err
}

if hubDetails.Name != sanitize.PathName(hubDetails.Name) {
return fmt.Errorf("err: invalid hub name '%s', potential path injection detected", hubDetails.Name)
}

//create the destination directory where the hub will be downloaded
hubPath := dirPath + "/" + hubDetails.Name + ".zip"
destDir, err := os.Create(hubPath)
Expand Down Expand Up @@ -278,6 +287,14 @@ func CopyZipItems(file *zip.File, extractPath string, chartsPath string) error {

// SyncRemoteRepo is used to sync the remote ChaosHub
func SyncRemoteRepo(hubData model.CloningInput, projectID string) error {
if projectID != sanitize.PathName(projectID) {
return fmt.Errorf("err: invalid projectID '%s', potential path injection detected", projectID)
}

if hubData.Name != sanitize.PathName(hubData.Name) {
return fmt.Errorf("err: invalid hub name '%s', potential path injection detected", hubData.Name)
}

hubPath := DefaultPath + projectID + "/" + hubData.Name
err := os.RemoveAll(hubPath)
if err != nil {
Expand Down Expand Up @@ -320,6 +337,34 @@ func ChaosHubIconHandler() gin.HandlerFunc {
responseStatusCode int
)

if c.Param("projectId") != sanitize.PathName(c.Param("projectId")) {
responseStatusCode = http.StatusBadRequest
log.Errorf("err: invalid projectID '%s', potential path injection detected", c.Param("projectId"))
fmt.Fprintf(c.Writer, "err: invalid projectID '%s', potential path injection detected", c.Param("projectId"))
return
}

if c.Param("hubName") != sanitize.PathName(c.Param("hubName")) {
responseStatusCode = http.StatusBadRequest
log.Errorf("err: invalid hub name '%s', potential path injection detected", c.Param("hubName"))
fmt.Fprintf(c.Writer, "err: invalid hub name '%s', potential path injection detected", c.Param("hubName"))
return
}

if c.Param("chartName") != sanitize.PathName(c.Param("chartName")) {
responseStatusCode = http.StatusBadRequest
log.Errorf("err: invalid chart name '%s', potential path injection detected", c.Param("chartName"))
fmt.Fprintf(c.Writer, "err: invalid chart name '%s', potential path injection detected", c.Param("chartName"))
return
}

if c.Param("iconName") != sanitize.PathName(c.Param("iconName")) {
responseStatusCode = http.StatusBadRequest
log.Errorf("err: invalid icon name '%s', potential path injection detected", c.Param("iconName"))
fmt.Fprintf(c.Writer, "err: invalid icon name '%s', potential path injection detected", c.Param("iconName"))
return
}

if strings.ToLower(c.Param("chartName")) == "predefined" {
img, err = os.Open(utils.Config.CustomChaosHubPath + c.Param("projectId") + "/" + c.Param("hubName") + "/experiments/icons/" + c.Param("iconName"))
responseStatusCode = http.StatusOK
Expand Down Expand Up @@ -354,6 +399,27 @@ func DefaultChaosHubIconHandler() gin.HandlerFunc {
responseStatusCode int
)

if c.Param("hubName") != sanitize.PathName(c.Param("hubName")) {
responseStatusCode = http.StatusBadRequest
log.Errorf("err: invalid hub name '%s', potential path injection detected", c.Param("hubName"))
fmt.Fprintf(c.Writer, "err: invalid hub name '%s', potential path injection detected", c.Param("hubName"))
return
}

if c.Param("chartName") != sanitize.PathName(c.Param("chartName")) {
responseStatusCode = http.StatusBadRequest
log.Errorf("err: invalid chart name '%s', potential path injection detected", c.Param("chartName"))
fmt.Fprintf(c.Writer, "err: invalid chart name '%s', potential path injection detected", c.Param("chartName"))
return
}

if c.Param("iconName") != sanitize.PathName(c.Param("iconName")) {
responseStatusCode = http.StatusBadRequest
log.Errorf("err: invalid icon name '%s', potential path injection detected", c.Param("iconName"))
fmt.Fprintf(c.Writer, "err: invalid icon name '%s', potential path injection detected", c.Param("iconName"))
return
}

if strings.ToLower(c.Param("chartName")) == "predefined" {
img, err = os.Open(utils.Config.DefaultChaosHubPath + c.Param("hubName") + "/experiments/icons/" + c.Param("iconName"))
responseStatusCode = http.StatusOK
Expand Down
18 changes: 18 additions & 0 deletions chaoscenter/graphql/server/pkg/chaoshub/handler/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,15 @@ func TestDownloadRemoteHub(t *testing.T) {
},
isError: true,
},
{
name: "path injection",
projectID: uuid.New().String(),
chaosHub: model.CreateRemoteChaosHub{
Name: uuid.New().String() + "../path/injection",
RepoURL: "https://github.com/litmuschaos/chaos-charts/archive/refs/heads/master.zip",
},
isError: true,
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
Expand Down Expand Up @@ -209,6 +218,15 @@ func TestSyncRemoteRepo(t *testing.T) {
},
isError: true,
},
{
name: "path injection",
projectID: uuid.New().String(),
chaosHub: model.CloningInput{
Name: uuid.New().String() + "../path/injection",
RepoURL: "https://github.com/litmuschaos/chaos-charts/archive/refs/heads/master.zip",
},
isError: true,
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
Expand Down
5 changes: 3 additions & 2 deletions chaoscenter/graphql/server/pkg/chaoshub/ops/gitops.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/litmuschaos/litmus/chaoscenter/graphql/server/graph/model"
"github.com/litmuschaos/litmus/chaoscenter/graphql/server/utils"
"github.com/mrz1836/go-sanitize"

"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
Expand Down Expand Up @@ -45,9 +46,9 @@ func GetClonePath(c ChaosHubConfig) string {

var repoPath string
if c.IsDefault {
repoPath = "/tmp/default/" + c.HubName
repoPath = "/tmp/default/" + sanitize.PathName(c.HubName)
} else {
repoPath = DefaultPath + c.ProjectID + "/" + c.HubName
repoPath = DefaultPath + sanitize.PathName(c.ProjectID) + "/" + sanitize.PathName(c.HubName)
}
return repoPath
}
Expand Down
52 changes: 44 additions & 8 deletions chaoscenter/graphql/server/pkg/chaoshub/ops/gitops_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,52 @@ func clearCloneRepository(projectID string) {

// TestGetClonePath is used to test the GetClonePath function
func TestGetClonePath(t *testing.T) {
// given
projectID := uuid.New().String()
chaosHubConfig := chaosHubOps.ChaosHubConfig{
ProjectID: projectID,
HubName: "test",

testCases := []struct {
name string
projectID string
hubName string
isDefault bool
expected string
}{
{
name: "default hub",
projectID: projectID,
hubName: "defaultHub",
isDefault: true,
expected: "/tmp/default/defaultHub",
},
{
name: "non-default hub",
projectID: projectID,
hubName: "testHub",
isDefault: false,
expected: "/tmp/" + projectID + "/testHub",
},
{
name: "path injection",
projectID: projectID,
hubName: "testHub/../Path/Injection",
isDefault: false,
expected: "/tmp/" + projectID + "/testHubPathInjection",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// given
chaosHubConfig := chaosHubOps.ChaosHubConfig{
ProjectID: tc.projectID,
HubName: tc.hubName,
IsDefault: tc.isDefault,
}
// when
path := chaosHubOps.GetClonePath(chaosHubConfig)
// then
assert.Equal(t, tc.expected, path)
})
}
// when
path := chaosHubOps.GetClonePath(chaosHubConfig)
// then
assert.Equal(t, "/tmp/"+projectID+"/test", path)
}

// TestGitConfigConstruct is used to test the GitConfigConstruct function
Expand Down
38 changes: 37 additions & 1 deletion chaoscenter/graphql/server/pkg/chaoshub/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/litmuschaos/litmus/chaoscenter/graphql/server/pkg/database/mongodb"
dbSchemaChaosHub "github.com/litmuschaos/litmus/chaoscenter/graphql/server/pkg/database/mongodb/chaos_hub"
"github.com/litmuschaos/litmus/chaoscenter/graphql/server/utils"
"github.com/mrz1836/go-sanitize"
log "github.com/sirupsen/logrus"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
Expand Down Expand Up @@ -329,6 +330,15 @@ func (c *chaosHubService) UpdateChaosHub(ctx context.Context, chaosHub model.Upd
if err != nil {
return nil, err
}

if prevChaosHub.ProjectID != sanitize.PathName(prevChaosHub.ProjectID) {
return nil, fmt.Errorf("err: invalid projectID '%s', potential path injection detected", projectID)
}

if prevChaosHub.Name != sanitize.PathName(prevChaosHub.Name) {
return nil, fmt.Errorf("err: invalid hub name '%s', potential path injection detected", chaosHub.Name)
}

clonePath := DefaultPath + prevChaosHub.ProjectID + "/" + prevChaosHub.Name
if prevChaosHub.HubType == string(model.HubTypeRemote) {
if prevChaosHub.Name != chaosHub.Name || prevChaosHub.RepoURL != chaosHub.RepoURL || prevChaosHub.RemoteHub != chaosHub.RemoteHub {
Expand Down Expand Up @@ -439,6 +449,15 @@ func (c *chaosHubService) DeleteChaosHub(ctx context.Context, hubID string, proj
log.Error(err)
return false, err
}

if projectID != sanitize.PathName(projectID) {
return false, fmt.Errorf("err: invalid projectID '%s', potential path injection detected", projectID)
}

if chaosHub.Name != sanitize.PathName(chaosHub.Name) {
return false, fmt.Errorf("err: invalid hub name '%s', potential path injection detected", chaosHub.Name)
}

clonePath := DefaultPath + projectID + "/" + chaosHub.Name
err = os.RemoveAll(clonePath)
if err != nil {
Expand Down Expand Up @@ -733,6 +752,14 @@ func (c *chaosHubService) ListPredefinedExperiments(ctx context.Context, hubID s
return nil, err
}

if projectID != sanitize.PathName(projectID) {
return nil, fmt.Errorf("err: invalid projectID '%s', potential path injection detected", projectID)
}

if hub.Name != sanitize.PathName(hub.Name) {
return nil, fmt.Errorf("err: invalid hub name '%s', potential path injection detected", hub.Name)
}

var hubPath string
if hub.IsDefault {
hubPath = "/tmp/default/" + hub.Name + "/experiments/"
Expand Down Expand Up @@ -792,6 +819,15 @@ func (c *chaosHubService) GetPredefinedExperiment(ctx context.Context, hubID str
if err != nil {
return nil, err
}

if projectID != sanitize.PathName(projectID) {
return nil, fmt.Errorf("err: invalid projectID '%s', potential path injection detected", projectID)
}

if hub.Name != sanitize.PathName(hub.Name) {
return nil, fmt.Errorf("err: invalid hub name '%s', potential path injection detected", hub.Name)
}

var hubPath string
if hub.IsDefault {
hubPath = "/tmp/default/" + hub.Name + "/experiments/"
Expand All @@ -814,7 +850,7 @@ func (c *chaosHubService) getPredefinedExperimentDetails(experimentsPath string,
var (
csvManifest = ""
workflowManifest = ""
path = experimentsPath + experiment + "/" + experiment + ".chartserviceversion.yaml"
path = experimentsPath + sanitize.PathName(experiment) + "/" + sanitize.PathName(experiment) + ".chartserviceversion.yaml"
isExist = true
preDefinedWorkflow = &model.PredefinedExperimentList{}
)
Expand Down
7 changes: 6 additions & 1 deletion chaoscenter/graphql/server/pkg/gitops/gitops.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"github.com/litmuschaos/litmus/chaoscenter/graphql/server/graph/model"
"github.com/litmuschaos/litmus/chaoscenter/graphql/server/pkg/authorization"
"github.com/litmuschaos/litmus/chaoscenter/graphql/server/pkg/database/mongodb/gitops"
"github.com/mrz1836/go-sanitize"
log "github.com/sirupsen/logrus"
ssh2 "golang.org/x/crypto/ssh"
)
Expand Down Expand Up @@ -84,7 +85,7 @@ func GetGitOpsConfig(repoData gitops.GitConfigDB) GitConfig {
RepositoryURL: repoData.RepositoryURL,
RemoteName: "origin",
Branch: repoData.Branch,
LocalPath: DefaultPath + repoData.ProjectID,
LocalPath: DefaultPath + sanitize.PathName(repoData.ProjectID),
LatestCommit: repoData.LatestCommit,
UserName: repoData.UserName,
Password: repoData.Password,
Expand All @@ -98,6 +99,10 @@ func GetGitOpsConfig(repoData gitops.GitConfigDB) GitConfig {

// setupGitRepo helps clones and sets up the repo for GitOps
func (c GitConfig) setupGitRepo(user GitUser) error {
if c.ProjectID != sanitize.PathName(c.ProjectID) {
return fmt.Errorf("err: invalid projectID '%s', potential path injection detected", c.ProjectID)
}

projectPath := c.LocalPath + "/" + ProjectDataPath + "/" + c.ProjectID

// clone repo
Expand Down
24 changes: 24 additions & 0 deletions chaoscenter/graphql/server/pkg/gitops/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"github.com/litmuschaos/litmus/chaoscenter/graphql/server/pkg/database/mongodb/chaos_infrastructure"
"github.com/litmuschaos/litmus/chaoscenter/graphql/server/pkg/database/mongodb/gitops"
"github.com/litmuschaos/litmus/chaoscenter/graphql/server/pkg/grpc"
"github.com/mrz1836/go-sanitize"
log "github.com/sirupsen/logrus"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
Expand Down Expand Up @@ -145,6 +146,10 @@ func (g *gitOpsService) EnableGitOpsHandler(ctx context.Context, projectID strin

// DisableGitOpsHandler disables GitOps for a specific project
func (g *gitOpsService) DisableGitOpsHandler(ctx context.Context, projectID string) (bool, error) {
if projectID != sanitize.PathName(projectID) {
return false, fmt.Errorf("err: invalid projectID '%s', potential path injection detected", projectID)
}

gitLock.Lock(projectID, nil)
defer gitLock.Unlock(projectID, nil)

Expand Down Expand Up @@ -183,6 +188,11 @@ func (g *gitOpsService) UpdateGitOpsDetailsHandler(ctx context.Context, projectI

gitConfig := GetGitOpsConfig(gitDB)
originalPath := gitConfig.LocalPath

if gitConfig.ProjectID != sanitize.PathName(gitConfig.ProjectID) {
return false, fmt.Errorf("err: invalid projectID '%s', potential path injection detected", gitConfig.ProjectID)
}

gitConfig.LocalPath = tempPath + gitConfig.ProjectID
commit, err := SetupGitOps(GitUserFromContext(ctx), gitConfig)
if err != nil {
Expand Down Expand Up @@ -319,6 +329,14 @@ func (g *gitOpsService) DeleteExperimentFromGit(ctx context.Context, projectID s
return errors.New("Sync Error | " + err.Error())
}

if gitConfig.ProjectID != sanitize.PathName(gitConfig.ProjectID) {
return fmt.Errorf("err: invalid projectID '%s', potential path injection detected", gitConfig.ProjectID)
}

if experiment.ExperimentName != sanitize.PathName(experiment.ExperimentName) {
return fmt.Errorf("err: invalid experiment name '%s', potential path injection detected", experiment.ExperimentName)
}

experimentPath := ProjectDataPath + "/" + gitConfig.ProjectID + "/" + experiment.ExperimentName + ".yaml"
exists, err := PathExists(gitConfig.LocalPath + "/" + experimentPath)
if err != nil {
Expand Down Expand Up @@ -456,6 +474,12 @@ func (g *gitOpsService) SyncDBToGit(ctx context.Context, config GitConfig) error
if !strings.HasSuffix(file, ".yaml") {
continue
}

if file != sanitize.PathName(file) {
log.Errorf("Invalid file name '%s', potential path injection detected", file)
continue
}

// check if file was deleted or not
exists, err := PathExists(config.LocalPath + "/" + file)
if err != nil {
Expand Down
Loading