diff --git a/internal/handlers/helper.go b/internal/handlers/helper.go index b231ae61d..45aaa4ac6 100644 --- a/internal/handlers/helper.go +++ b/internal/handlers/helper.go @@ -2,6 +2,8 @@ package httphandlers import ( + "strings" + "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/poyrazk/thecloud/internal/errors" @@ -53,5 +55,12 @@ func getBucketAndKeyRequired(c *gin.Context) (bucket, key string, ok bool) { return "", "", false } + // Prevent path traversal attacks (e.g., "../../../etc/passwd") + // Note: Gin glob pattern *key captures the path with leading /, so we only check for ".." + if strings.Contains(key, "..") { + httputil.Error(c, errors.New(errors.InvalidInput, "invalid key")) + return "", "", false + } + return bucket, key, true } diff --git a/internal/handlers/helper_test.go b/internal/handlers/helper_test.go index 29363b01b..3f13d8889 100644 --- a/internal/handlers/helper_test.go +++ b/internal/handlers/helper_test.go @@ -120,4 +120,28 @@ func TestGetBucketAndKeyRequired(t *testing.T) { assert.Equal(t, http.StatusBadRequest, w.Code) assert.Contains(t, w.Body.String(), "key is required") }) + + t.Run("path traversal rejected", func(t *testing.T) { + traversalKeys := []string{ + "../etc/passwd", + "foo/../bar", + "foo\\..\\bar", + "foo/../../etc/passwd", + } + for _, key := range traversalKeys { + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Params = []gin.Param{ + {Key: "bucket", Value: testBucket}, + {Key: "key", Value: key}, + } + + bucket, key, ok := getBucketAndKeyRequired(c) + + assert.False(t, ok, "key %q should be rejected", key) + assert.Empty(t, bucket) + assert.Empty(t, key) + assert.Equal(t, http.StatusBadRequest, w.Code) + } + }) }