diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index fe3cdb2..97b7cc2 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -174,6 +174,60 @@ SSL termination and request routing. │◀─────────────────────────────│ ``` +## Security Architecture + +### Authentication & Authorization + +- **Passkeys (WebAuthn)**: Primary authentication method using FIDO2/U2F security keys +- **JWT Tokens**: Session-based tokens with configurable expiration +- **Role-Based Access Control (RBAC)**: Owner, Admin, Member roles for organizations +- **Permission System**: Granular permissions for file operations (read, write, view, edit) + +### Input Validation & Sanitization + +- **Path Traversal Protection**: All file paths are sanitized to prevent directory traversal attacks +- **UUID Validation**: All resource IDs (users, orgs, files) are validated as proper UUIDs +- **JSON Schema Validation**: API inputs are validated for correct structure and types + +### Network Security + +- **HTTPS Only**: All external traffic is encrypted via TLS +- **CORS Policy**: Restricted to allowed origins with credentials support +- **Rate Limiting**: 100 requests/minute general, 10 requests/minute for auth endpoints +- **Security Headers**: + - `X-Content-Type-Options: nosniff` + - `X-Frame-Options: DENY` (except for WOPI/Collabora) + - `X-XSS-Protection: 1; mode=block` + - `Content-Security-Policy`: Restrictive policy allowing only necessary sources + - `Referrer-Policy: strict-origin-when-cross-origin` + +### Data Protection + +- **Encrypted Storage**: Files stored encrypted in Nextcloud +- **Secure Passwords**: Auto-generated secure passwords for Nextcloud user accounts +- **Audit Logging**: All operations logged with user/org context +- **No Secrets in Logs**: Sensitive data never logged + +### API Security + +- **Token Validation**: Every protected endpoint validates JWT tokens +- **Session Management**: Secure session handling with database-backed validation +- **Error Handling**: Safe error responses that don't leak internal details + +### File Security + +- **Scoped Access**: Users can only access files within their personal workspace or authorized organizations +- **Share Tokens**: Public shares use short-lived, single-use tokens +- **Nextcloud Integration**: Leverages Nextcloud's security features for file access + +### Infrastructure Security + +- **Container Security**: Docker images run as non-root where possible +- **Network Isolation**: Internal Docker networks prevent direct external access +- **Deployment Security**: Automated deployments with health checks + +## Data Flow + ### File Upload Flow ``` diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 393debe..bfa7218 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -25,6 +25,34 @@ This guide covers local development setup, coding conventions, and contribution - **TablePlus** or **DBeaver** for database management - **Postman** or **Bruno** for API testing +## Security Guidelines + +### Code Security + +- **Never log secrets**: Passwords, tokens, keys must never appear in logs +- **Validate all inputs**: Use `sanitizePath()` for file paths, validate UUIDs +- **Use structured errors**: Return safe error messages that don't leak internal details +- **HTTPS only**: All API calls must use HTTPS in production +- **Input sanitization**: All user inputs must be validated and sanitized + +### Authentication + +- **JWT tokens**: Use secure, short-lived tokens +- **Session validation**: Always validate sessions against database +- **Passkey security**: Follow WebAuthn best practices + +### File Operations + +- **Path validation**: Prevent directory traversal with proper path sanitization +- **Permission checks**: Verify user permissions before file operations +- **Scoped access**: Users can only access authorized files/orgs + +### Development Security + +- **Local secrets**: Use `.env` files, never commit secrets +- **Test with security**: Include security tests in development +- **Review code**: Security review for all changes + ## Project Setup ### 1. Clone the Repository diff --git a/docs/SECURITY.md b/docs/SECURITY.md index 5a954c7..be302a2 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -98,6 +98,17 @@ This document describes the security architecture, configurations, and best prac - `INTERNAL` (500) - **No Secrets in Logs**: Passwords and tokens are never logged +### Security Headers + +The application sets comprehensive security headers: + +- **X-Content-Type-Options**: `nosniff` - Prevents MIME type sniffing +- **X-Frame-Options**: `DENY` - Prevents clickjacking (except for WOPI endpoints) +- **X-XSS-Protection**: `1; mode=block` - Enables XSS filtering +- **Content-Security-Policy**: Restrictive policy allowing only necessary sources +- **Referrer-Policy**: `strict-origin-when-cross-origin` - Controls referrer information +- **CORS**: Restricted to allowed origins with credentials support + ## Network Security ### TLS Configuration diff --git a/go_cloud/api b/go_cloud/api index 8ef8f49..d8ab50e 100755 Binary files a/go_cloud/api and b/go_cloud/api differ diff --git a/go_cloud/internal/config/config.go b/go_cloud/internal/config/config.go index 71bf55c..e8f24a0 100644 --- a/go_cloud/internal/config/config.go +++ b/go_cloud/internal/config/config.go @@ -1,7 +1,7 @@ package config import ( - "fmt" + "log" "os" ) @@ -35,7 +35,7 @@ func Load() *Config { NextcloudBase: getEnv("NEXTCLOUD_BASEPATH", "/"), AllowedOrigins: getEnv("ALLOWED_ORIGINS", "https://b0esche.cloud,https://www.b0esche.cloud,https://*.b0esche.cloud,http://localhost:8080"), } - fmt.Printf("[CONFIG] Nextcloud URL: %q, User: %q, BasePath: %q\n", cfg.NextcloudURL, cfg.NextcloudUser, cfg.NextcloudBase) + log.Printf("[CONFIG] Nextcloud URL: %q, User: %q, BasePath: %q\n", cfg.NextcloudURL, cfg.NextcloudUser, cfg.NextcloudBase) return cfg } diff --git a/go_cloud/internal/http/routes.go b/go_cloud/internal/http/routes.go index 5bf23ea..a80475e 100644 --- a/go_cloud/internal/http/routes.go +++ b/go_cloud/internal/http/routes.go @@ -91,7 +91,7 @@ func getUserWebDAVClient(ctx context.Context, db *database.DB, userID uuid.UUID, user.NextcloudUsername = ncUsername user.NextcloudPassword = ncPassword - fmt.Printf("[AUTO-PROVISION] Created Nextcloud account for user %s: %s\n", userID, ncUsername) + log.Printf("[AUTO-PROVISION] Created Nextcloud account for user %s: %s\n", userID, ncUsername) } // Create user-specific WebDAV client @@ -106,6 +106,7 @@ func NewRouter(cfg *config.Config, db *database.DB, jwtManager *jwt.Manager, aut r.Use(middleware.Logger) r.Use(middleware.Recoverer) r.Use(middleware.CORS(cfg.AllowedOrigins)) + r.Use(middleware.SecurityHeaders()) r.Use(middleware.RateLimit) // Health check @@ -571,6 +572,11 @@ func listFilesHandler(w http.ResponseWriter, r *http.Request, db *database.DB) { if path == "" { path = "/" } + path, err = sanitizePath(path) + if err != nil { + errors.WriteError(w, errors.CodeInvalidArgument, "Invalid path", http.StatusBadRequest) + return + } q := r.URL.Query().Get("q") page := 1 pageSize := 100 @@ -1871,6 +1877,11 @@ func createOrgFileHandler(w http.ResponseWriter, r *http.Request, db *database.D if parentPath == "" { parentPath = "/" } + parentPath, err = sanitizePath(parentPath) + if err != nil { + errors.WriteError(w, errors.CodeInvalidArgument, "Invalid path", http.StatusBadRequest) + return + } var file multipart.File var header *multipart.FileHeader file, header, err = r.FormFile("file") @@ -1972,6 +1983,13 @@ func deleteOrgFileHandler(w http.ResponseWriter, r *http.Request, db *database.D return } + var err error + req.Path, err = sanitizePath(req.Path) + if err != nil { + errors.WriteError(w, errors.CodeInvalidArgument, "Invalid path", http.StatusBadRequest) + return + } + // Get or create user's WebDAV client and delete from Nextcloud storageClient, err := getUserWebDAVClient(r.Context(), db, userID, cfg.NextcloudURL, cfg.NextcloudUser, cfg.NextcloudPass) if err != nil { @@ -2022,6 +2040,18 @@ func moveOrgFileHandler(w http.ResponseWriter, r *http.Request, db *database.DB, return } + var err error + req.SourcePath, err = sanitizePath(req.SourcePath) + if err != nil { + errors.WriteError(w, errors.CodeInvalidArgument, "Invalid source path", http.StatusBadRequest) + return + } + req.TargetPath, err = sanitizePath(req.TargetPath) + if err != nil { + errors.WriteError(w, errors.CodeInvalidArgument, "Invalid target path", http.StatusBadRequest) + return + } + // Get source file details before moving sourceFiles, err := db.GetOrgFiles(r.Context(), orgID, userID, "/", "", 0, 1000) if err != nil { @@ -2134,6 +2164,11 @@ func createUserFileHandler(w http.ResponseWriter, r *http.Request, db *database. if parentPath == "" { parentPath = "/" } + parentPath, err = sanitizePath(parentPath) + if err != nil { + errors.WriteError(w, errors.CodeInvalidArgument, "Invalid path", http.StatusBadRequest) + return + } var file multipart.File var header *multipart.FileHeader file, header, err = r.FormFile("file") @@ -2242,6 +2277,18 @@ func moveUserFileHandler(w http.ResponseWriter, r *http.Request, db *database.DB return } + var err error + req.SourcePath, err = sanitizePath(req.SourcePath) + if err != nil { + errors.WriteError(w, errors.CodeInvalidArgument, "Invalid source path", http.StatusBadRequest) + return + } + req.TargetPath, err = sanitizePath(req.TargetPath) + if err != nil { + errors.WriteError(w, errors.CodeInvalidArgument, "Invalid target path", http.StatusBadRequest) + return + } + // Get source file details before moving sourceFile, err := db.GetUserFileByPath(r.Context(), userID, req.SourcePath) if err != nil { @@ -2339,6 +2386,13 @@ func deleteUserFileHandler(w http.ResponseWriter, r *http.Request, db *database. return } + var err error + req.Path, err = sanitizePath(req.Path) + if err != nil { + errors.WriteError(w, errors.CodeInvalidArgument, "Invalid path", http.StatusBadRequest) + return + } + // Get or create user's WebDAV client and delete from Nextcloud storageClient, err := getUserWebDAVClient(r.Context(), db, userID, cfg.NextcloudURL, cfg.NextcloudUser, cfg.NextcloudPass) if err != nil { diff --git a/go_cloud/internal/middleware/middleware.go b/go_cloud/internal/middleware/middleware.go index 8f2b808..eb12df3 100644 --- a/go_cloud/internal/middleware/middleware.go +++ b/go_cloud/internal/middleware/middleware.go @@ -25,6 +25,27 @@ var RequestID = middleware.RequestID var Logger = middleware.Logger var Recoverer = middleware.Recoverer +// SecurityHeaders adds security-related HTTP headers +func SecurityHeaders() func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Prevent MIME type sniffing + w.Header().Set("X-Content-Type-Options", "nosniff") + // Prevent clickjacking - allow for WOPI routes + if !strings.HasPrefix(r.URL.Path, "/wopi") && !strings.HasPrefix(r.URL.Path, "/user/files/") && !strings.HasPrefix(r.URL.Path, "/orgs/") { + w.Header().Set("X-Frame-Options", "DENY") + } + // Enable XSS protection + w.Header().Set("X-XSS-Protection", "1; mode=block") + // Referrer policy + w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin") + // Content Security Policy - basic policy + w.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https://go.b0esche.cloud https://of.b0esche.cloud; frame-src 'self' https://of.b0esche.cloud;") + next.ServeHTTP(w, r) + }) + } +} + // CORS middleware - accepts allowedOrigins comma-separated string func CORS(allowedOrigins string) func(http.Handler) http.Handler { allowedList, allowAll := compileAllowedOrigins(allowedOrigins) diff --git a/go_cloud/internal/storage/nextcloud.go b/go_cloud/internal/storage/nextcloud.go index 89088f9..fade96b 100644 --- a/go_cloud/internal/storage/nextcloud.go +++ b/go_cloud/internal/storage/nextcloud.go @@ -6,6 +6,7 @@ import ( "encoding/base64" "fmt" "io" + "log" "net/http" "net/url" "strings" @@ -47,7 +48,7 @@ func CreateNextcloudUser(nextcloudBaseURL, adminUser, adminPass, username, passw return fmt.Errorf("failed to create Nextcloud user (status %d): %s", resp.StatusCode, string(body)) } - fmt.Printf("[NEXTCLOUD] Created user account: %s with generated password\n", username) + log.Printf("[NEXTCLOUD] Created user account: %s with generated password\n", username) return nil } diff --git a/go_cloud/internal/storage/webdav.go b/go_cloud/internal/storage/webdav.go index dbf65ea..9e74492 100644 --- a/go_cloud/internal/storage/webdav.go +++ b/go_cloud/internal/storage/webdav.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "io" + "log" "net/http" "net/url" "path" @@ -24,7 +25,7 @@ type WebDAVClient struct { // NewWebDAVClient returns nil if no Nextcloud URL configured func NewWebDAVClient(cfg *config.Config) *WebDAVClient { if cfg == nil || strings.TrimSpace(cfg.NextcloudURL) == "" { - fmt.Printf("[WEBDAV] No Nextcloud URL configured, WebDAV client is nil\n") + log.Printf("[WEBDAV] No Nextcloud URL configured, WebDAV client is nil\n") return nil } u := strings.TrimRight(cfg.NextcloudURL, "/") @@ -32,7 +33,7 @@ func NewWebDAVClient(cfg *config.Config) *WebDAVClient { if base == "" { base = "/" } - fmt.Printf("[WEBDAV] Initializing WebDAV client - URL: %s, User: %s, BasePath: %s\n", u, cfg.NextcloudUser, base) + log.Printf("[WEBDAV] Initializing WebDAV client - URL: %s, User: %s, BasePath: %s\n", u, cfg.NextcloudUser, base) return &WebDAVClient{ baseURL: u, user: cfg.NextcloudUser,