diff --git a/server/internal/handlers/file_handlers.go b/server/internal/handlers/file_handlers.go index 3f53c17..93ddd99 100644 --- a/server/internal/handlers/file_handlers.go +++ b/server/internal/handlers/file_handlers.go @@ -4,8 +4,10 @@ import ( "encoding/json" "io" "net/http" + "os" "novamd/internal/context" + "novamd/internal/storage" "github.com/go-chi/chi/v5" ) @@ -63,7 +65,18 @@ func (h *Handler) GetFileContent() http.HandlerFunc { filePath := chi.URLParam(r, "*") content, err := h.Storage.GetFileContent(ctx.UserID, ctx.Workspace.ID, filePath) if err != nil { - http.Error(w, "Failed to read file", http.StatusNotFound) + + if storage.IsPathValidationError(err) { + http.Error(w, "Invalid file path", http.StatusBadRequest) + return + } + + if os.IsNotExist(err) { + http.Error(w, "Failed to read file", http.StatusNotFound) + return + } + + http.Error(w, "Failed to read file", http.StatusInternalServerError) return } @@ -93,6 +106,11 @@ func (h *Handler) SaveFile() http.HandlerFunc { err = h.Storage.SaveFile(ctx.UserID, ctx.Workspace.ID, filePath, content) if err != nil { + if storage.IsPathValidationError(err) { + http.Error(w, "Invalid file path", http.StatusBadRequest) + return + } + http.Error(w, "Failed to save file", http.StatusInternalServerError) return } @@ -112,6 +130,16 @@ func (h *Handler) DeleteFile() http.HandlerFunc { filePath := chi.URLParam(r, "*") err := h.Storage.DeleteFile(ctx.UserID, ctx.Workspace.ID, filePath) if err != nil { + if storage.IsPathValidationError(err) { + http.Error(w, "Invalid file path", http.StatusBadRequest) + return + } + + if os.IsNotExist(err) { + http.Error(w, "File not found", http.StatusNotFound) + return + } + http.Error(w, "Failed to delete file", http.StatusInternalServerError) return } @@ -165,10 +193,21 @@ func (h *Handler) UpdateLastOpenedFile() http.HandlerFunc { return } - // Validate the file path exists in the workspace + // Validate the file path in the workspace if requestBody.FilePath != "" { - if _, err := h.Storage.ValidatePath(ctx.UserID, ctx.Workspace.ID, requestBody.FilePath); err != nil { - http.Error(w, "Invalid file path", http.StatusBadRequest) + _, err := h.Storage.GetFileContent(ctx.UserID, ctx.Workspace.ID, requestBody.FilePath) + if err != nil { + if storage.IsPathValidationError(err) { + http.Error(w, "Invalid file path", http.StatusBadRequest) + return + } + + if os.IsNotExist(err) { + http.Error(w, "File not found", http.StatusNotFound) + return + } + + http.Error(w, "Failed to update file", http.StatusInternalServerError) return } } diff --git a/server/internal/storage/errors.go b/server/internal/storage/errors.go new file mode 100644 index 0000000..dbfdf02 --- /dev/null +++ b/server/internal/storage/errors.go @@ -0,0 +1,24 @@ +// storage/errors.go + +package storage + +import ( + "errors" + "fmt" +) + +// PathValidationError represents a path validation error (e.g., path traversal attempt) +type PathValidationError struct { + Path string + Message string +} + +func (e *PathValidationError) Error() string { + return fmt.Sprintf("%s: %s", e.Message, e.Path) +} + +// IsPathValidationError checks if the error is a PathValidationError +func IsPathValidationError(err error) bool { + var pathErr *PathValidationError + return err != nil && errors.As(err, &pathErr) +} diff --git a/server/internal/storage/workspace.go b/server/internal/storage/workspace.go index 7a264ad..2a4beca 100644 --- a/server/internal/storage/workspace.go +++ b/server/internal/storage/workspace.go @@ -27,7 +27,7 @@ func (s *Service) ValidatePath(userID, workspaceID int, path string) (string, er // First check if the path is absolute if filepath.IsAbs(path) { - return "", fmt.Errorf("invalid path: absolute paths not allowed") + return "", &PathValidationError{Path: path, Message: "absolute paths not allowed"} } // Join and clean the path @@ -36,7 +36,7 @@ func (s *Service) ValidatePath(userID, workspaceID int, path string) (string, er // Verify the path is still within the workspace if !strings.HasPrefix(cleanPath, workspacePath) { - return "", fmt.Errorf("invalid path: outside of workspace") + return "", &PathValidationError{Path: path, Message: "path traversal attempt"} } return cleanPath, nil diff --git a/server/internal/storage/workspace_test.go b/server/internal/storage/workspace_test.go index f6b1607..2fc11b2 100644 --- a/server/internal/storage/workspace_test.go +++ b/server/internal/storage/workspace_test.go @@ -48,7 +48,7 @@ func TestValidatePath(t *testing.T) { path: "../../../etc/passwd", want: "", wantErr: true, - errContains: "outside of workspace", + errContains: "path traversal attempt", }, { name: "absolute path attempt",