diff --git a/README.md b/README.md index c2de984..bf15477 100644 --- a/README.md +++ b/README.md @@ -11,11 +11,15 @@ selected branch (in variable REPO_BRANCH) and can be pulled new version on defin ## RELEASE NOTES: v0.0.3-alpha -| Feature | Description | -|---------|---------------------------------------------------| -| Done | Add support to proxy redirect with HTTPS backends | -| FIX | Bump golang.org/x/text from 0.3.7 to 0.3.8 | - +| Feature | Description | +|---------|--------------------------------------------------------------------------------------------------------------------| +| Done | Add support to proxy redirect with HTTPS backends | +| FIXED | Bump golang.org/x/text from 0.3.7 to 0.3.8 | +| FIXED | Bump github.com/stretchr/testify from 1.7.0 to 1.7.1 | +| FIXED | Bump golang.org/x/sys from 0.0.0-20220209214540-3681064d5158 to 0.1.0 | +| FIXED | Bump golang.org/x/crypto from 0.0.0-20220214200702-86341886e292 to 0.1.0 | +| FIXED | Bump golang.org/x/net from 0.0.0-20220127200216-cd36cc0744dd to 0.17.0 in | +| FIXED | Vulnerability issue Uncontrolled data used in path [Issue](https://github.com/jarpsimoes/git-http-server/issues/8) | ## Authentication Methods @@ -25,22 +29,22 @@ The GIT-HttpServer only support basic authentication on repositories by protocol ## Configuration ### Environment Variables -| Name | Description | Default | Mandatory | -|---------------------------|----------------------------------------------------------|---------------------------------------------------|-----------| -| PATH_CLONE | Set clone path | _clone | Yes | -| PATH_PULL | Set pull path | _pull | Yes | -| PATH_VERSION | Set get git commit version path | _version | Yes | -| PATH_WEBHOOK | Set webhook path | _hook | Yes | -| PATH_HEALTH | Set health check path | _health | Yes | -| REPO_BRANCH | Set default branch to clone content | main | No | -| REPO_TARGET_FOLDER | Set folder to clone source | target-git | No | -| REPO_URL | Set url as a source origin | https://github.com/jarpsimoes/git-http-server.git | No | -| REPO_USERNAME | Set username or token identifier to basic authentication | N/D | No | -| REPO_PASSWORD | Set password or token to basic authentication | N/D | No | -| HTTP_PORT | Set port to expose content | 8081 | Yes | -| GHS_CUSTOM_PATH_ | Custom path to work as a proxy server | N/D | No | -| GHS_CUSTOM_REWRITE_ | Set to remove from proxy request base path | N/D | No | -| FOLDER_ROOT | Select root folder inside cloned repository | $REPO_TARGET_FOLDER/ | No | +| Name | Description | Default | Mandatory | +|---------------------------|------------------------------------------------------------------------|---------------------------------------------------|-----------| +| PATH_CLONE | Set clone path | _clone | Yes | +| PATH_PULL | Set pull path | _pull | Yes | +| PATH_VERSION | Set get git commit version path | _version | Yes | +| PATH_WEBHOOK | Set webhook path | _hook | Yes | +| PATH_HEALTH | Set health check path | _health | Yes | +| REPO_BRANCH | Set default branch to clone content | main | No | +| REPO_TARGET_FOLDER | Set folder to clone source (only allowed letters, numbers, underscore) | target-git | No | +| REPO_URL | Set url as a source origin | https://github.com/jarpsimoes/git-http-server.git | No | +| REPO_USERNAME | Set username or token identifier to basic authentication | N/D | No | +| REPO_PASSWORD | Set password or token to basic authentication | N/D | No | +| HTTP_PORT | Set port to expose content | 8081 | Yes | +| GHS_CUSTOM_PATH_ | Custom path to work as a proxy server | N/D | No | +| GHS_CUSTOM_REWRITE_ | Set to remove from proxy request base path | N/D | No | +| FOLDER_ROOT **(Removed)** | The base path will be always root folder of the application | REMOVED | N/D | ## Implementation diff --git a/src/utils/custom_path_test.go b/src/utils/custom_path_test.go index 85e6cba..0046d6a 100644 --- a/src/utils/custom_path_test.go +++ b/src/utils/custom_path_test.go @@ -11,8 +11,9 @@ import ( func TestGetAllCustomPaths(t *testing.T) { os.Setenv("GHS_CUSTOM_PATH_images/digilex-infordoc-images", "https://storage.gra.cloud.ovh.net/v1/AUTH_ed33ec9e34c64b54aca49d6fcb6dc4c8/infordoc-img") os.Setenv("GHS_CUSTOM_REWRITE_images/digilex-infordoc-images", "") - os.Setenv("GHS_CUSTOM_PATH_images.digilex-infordoc-images-1", "https://storage.gra.cloud.ovh.net/v1/AUTH_ed33ec9e34c64b54aca49d6fcb6dc4c8/img") - os.Setenv("GHS_CUSTOM_PATH_/images/digilex-infordoc-images-2", "https://storage.gra.cloud.ovh.net/v1/AUTH_ed33ec9e34c64b54aca49d6fcb6dc4c8/infordoc") + os.Setenv("GHS_CUSTOM_PATH_images.digilex-infordoc/1", "https://storage.gra.cloud.ovh.net/v1/AUTH_ed33ec9e34c64b54aca49d6fcb6dc4c8/img") + os.Setenv("GHS_CUSTOM_PATH_/images/digilex-infordoc-img", "https://storage.gra.cloud.ovh.net/v1/AUTH_ed33ec9e34c64b54aca49d6fcb6dc4c8/infordoc") + result := GetCustomPathsInstance() assert.True(t, len(*result) == 3) @@ -25,3 +26,32 @@ func TestGetAllCustomPaths(t *testing.T) { } } +func TestFindPath(t *testing.T) { + os.Setenv("GHS_CUSTOM_PATH_images/digilex-infordoc-images", "https://storage.gra.cloud.ovh.net/v1/AUTH_ed33ec9e34c64b54aca49d6fcb6dc4c8/infordoc-img") + os.Setenv("GHS_CUSTOM_REWRITE_images/digilex-infordoc-images", "") + os.Setenv("GHS_CUSTOM_PATH_images.digilex-infordoc/1", "https://storage.gra.cloud.ovh.net/v1/AUTH_ed33ec9e34c64b54aca49d6fcb6dc4c8/img") + os.Setenv("GHS_CUSTOM_PATH_/images/digilex-infordoc-img", "https://storage.gra.cloud.ovh.net/v1/AUTH_ed33ec9e34c64b54aca49d6fcb6dc4c8/infordoc") + + result, path := FindPath("/images/digilex-infordoc-images") + + assert.True(t, result) + assert.True(t, path.GetTarget() == "https://storage.gra.cloud.ovh.net/v1/AUTH_ed33ec9e34c64b54aca49d6fcb6dc4c8/infordoc-img") + assert.True(t, path.IsRewrite()) + + result1, path1 := FindPath("/images/digilex-infordoc/1") + + assert.True(t, result1) + assert.True(t, path1.GetTarget() == "https://storage.gra.cloud.ovh.net/v1/AUTH_ed33ec9e34c64b54aca49d6fcb6dc4c8/img") + assert.True(t, path1.IsRewrite() == false) + + result2, path2 := FindPath("/images/digilex-infordoc-img") + + assert.True(t, result2) + assert.True(t, path2.GetTarget() == "https://storage.gra.cloud.ovh.net/v1/AUTH_ed33ec9e34c64b54aca49d6fcb6dc4c8/infordoc") + assert.True(t, path2.IsRewrite() == false) + + result3, _ := FindPath("/images/test/digilex-infordoc-img") + + assert.False(t, result3) + +} diff --git a/src/utils/environment_config.go b/src/utils/environment_config.go index c8489e1..c1c399a 100644 --- a/src/utils/environment_config.go +++ b/src/utils/environment_config.go @@ -6,6 +6,8 @@ import ( "github.com/go-git/go-git/v5/plumbing/transport/http" "log" "os" + "path/filepath" + "runtime" "sync" "time" ) @@ -51,6 +53,11 @@ var baseRouteConfigInstance *BaseRouteConfig var baseRepositoryConfigInstance *BaseRepositoryConfig var basicAuthenticationMethod *BasicAuthenticationMethod var healthCheckControl *HealthCheckControl +var pathSecurityCheck *PathSecurityCheck +var ( + _, b, _, _ = runtime.Caller(0) + basePath = filepath.Dir(b) +) // UpdateState [HealthCheckControl] it's a function to update Status func (hcc *HealthCheckControl) UpdateState(status bool) { @@ -183,9 +190,16 @@ func GetRepositoryConfigInstance() *BaseRepositoryConfig { repoURL: os.Getenv("REPO_URL"), branch: os.Getenv("REPO_BRANCH"), targetFolder: os.Getenv("REPO_TARGET_FOLDER"), - rootFolder: os.Getenv("FOLDER_ROOT"), + rootFolder: basePath, } + if pathSecurityCheck.IsValidPath(baseRepositoryConfigInstance.targetFolder) { + log.Printf("[BaseRepositoryConfigInstance] Target folder %v is valid", baseRepositoryConfigInstance.targetFolder) + } else { + log.Printf("[BaseRepositoryConfigInstance] Target folder %v is invalid", baseRepositoryConfigInstance.targetFolder) + log.Printf("[BaseRepositoryConfigInstance] Will be changed to target") + baseRepositoryConfigInstance.targetFolder = "target_folder" + } } } diff --git a/src/utils/environment_config_test.go b/src/utils/environment_config_test.go index a248a64..de816e0 100644 --- a/src/utils/environment_config_test.go +++ b/src/utils/environment_config_test.go @@ -43,6 +43,17 @@ func TestGetRepositoryConfigInstance(t *testing.T) { assert.Equal(t, "https://test.com/repo.git", repositoryConfigInstance1.GetRepo(), "Check repo url") assert.Equal(t, "main", repositoryConfigInstance1.GetBranch(), "Check repo url") assert.Equal(t, "test1", repositoryConfigInstance1.GetTargetFolder(), "Check repo url") + +} +func TestGetRepositoryConfigInstanceInvalid(t *testing.T) { + baseRepositoryConfigInstance = nil + + os.Setenv("REPO_URL", "https://test.com/repo.git") + os.Setenv("REPO_BRANCH", "main") + os.Setenv("REPO_TARGET_FOLDER", "test1/test2") + + repositoryConfigInstance2 := GetRepositoryConfigInstance() + assert.Equal(t, "target_folder", repositoryConfigInstance2.GetTargetFolder(), "Check repo url") } func TestGetBasicAuthenticationMethodInstance(t *testing.T) { os.Setenv("REPO_USERNAME", os.Getenv("ACCESS_USERNAME")) diff --git a/src/utils/path_security_check.go b/src/utils/path_security_check.go new file mode 100644 index 0000000..b27ba26 --- /dev/null +++ b/src/utils/path_security_check.go @@ -0,0 +1,61 @@ +package utils + +import ( + "regexp" +) + +type PathSecurityCheck struct { +} + +// IsValidPath checks if the input string adheres to the specified rules. +func (psc *PathSecurityCheck) IsValidPath(input string) bool { + // Rule 1: Do not allow more than a single "." character. + if countDots := psc.countOccurrences(input, '.'); countDots > 1 { + return false + } + + if psc.containsDirectorySeparator(input) { + return false + } + + allowList := []*regexp.Regexp{ + regexp.MustCompile(`^[a-zA-Z0-9_-]+$`), // Alphanumeric characters, underscore, and hyphen. + regexp.MustCompile(`^([a-zA-Z0-9_-]+\.){0,2}[a-zA-Z0-9_-]+$`), // Allow up to two dots in the filename. + } + + for _, pattern := range allowList { + if pattern.MatchString(input) { + return true + } + } + + return false +} + +// countOccurrences counts the occurrences of a specific character in a string. +func (psc *PathSecurityCheck) countOccurrences(s string, c byte) int { + count := 0 + for i := 0; i < len(s); i++ { + if s[i] == c { + count++ + } + } + return count +} + +// containsDirectorySeparator checks if the input string contains directory separators. +func (psc *PathSecurityCheck) containsDirectorySeparator(s string) bool { + return psc.containsAny(s, []byte{'/', '\\'}) +} + +// containsAny checks if the input string contains any of the specified characters. +func (psc *PathSecurityCheck) containsAny(s string, chars []byte) bool { + for i := 0; i < len(s); i++ { + for _, c := range chars { + if s[i] == c { + return true + } + } + } + return false +} diff --git a/src/utils/path_security_test.go b/src/utils/path_security_test.go new file mode 100644 index 0000000..a2e180b --- /dev/null +++ b/src/utils/path_security_test.go @@ -0,0 +1,33 @@ +package utils + +import ( + "testing" +) + +func TestIsValidPath(t *testing.T) { + tests := []struct { + input string + expected bool + }{ + {"valid_path123", true}, + {"invalid_path/with_slash", false}, + {"invalid_path/with/slash", false}, + {"invalid/path/with/slash", false}, + {"invalid.path.with.multiple.dots", false}, + {"another.invalid.path.with.multiple.dots", false}, + {"valid_path_with_underscore", true}, + {"valid-path_with-hyphen", true}, + {"valid.path_with_underscore-hyphen", true}, + } + + pathSecurityCheck := PathSecurityCheck{} + + for _, test := range tests { + t.Run(test.input, func(t *testing.T) { + result := pathSecurityCheck.IsValidPath(test.input) + if result != test.expected { + t.Errorf("Expected IsValidPath(%s) to be %v, but got %v", test.input, test.expected, result) + } + }) + } +}