Enhance security architecture and guidelines across documentation and middleware; implement input validation, logging improvements, and security headers in API handlers.
This commit is contained in:
@@ -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
|
### File Upload Flow
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -25,6 +25,34 @@ This guide covers local development setup, coding conventions, and contribution
|
|||||||
- **TablePlus** or **DBeaver** for database management
|
- **TablePlus** or **DBeaver** for database management
|
||||||
- **Postman** or **Bruno** for API testing
|
- **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
|
## Project Setup
|
||||||
|
|
||||||
### 1. Clone the Repository
|
### 1. Clone the Repository
|
||||||
|
|||||||
@@ -98,6 +98,17 @@ This document describes the security architecture, configurations, and best prac
|
|||||||
- `INTERNAL` (500)
|
- `INTERNAL` (500)
|
||||||
- **No Secrets in Logs**: Passwords and tokens are never logged
|
- **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
|
## Network Security
|
||||||
|
|
||||||
### TLS Configuration
|
### TLS Configuration
|
||||||
|
|||||||
BIN
go_cloud/api
BIN
go_cloud/api
Binary file not shown.
@@ -1,7 +1,7 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ func Load() *Config {
|
|||||||
NextcloudBase: getEnv("NEXTCLOUD_BASEPATH", "/"),
|
NextcloudBase: getEnv("NEXTCLOUD_BASEPATH", "/"),
|
||||||
AllowedOrigins: getEnv("ALLOWED_ORIGINS", "https://b0esche.cloud,https://www.b0esche.cloud,https://*.b0esche.cloud,http://localhost:8080"),
|
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
|
return cfg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ func getUserWebDAVClient(ctx context.Context, db *database.DB, userID uuid.UUID,
|
|||||||
|
|
||||||
user.NextcloudUsername = ncUsername
|
user.NextcloudUsername = ncUsername
|
||||||
user.NextcloudPassword = ncPassword
|
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
|
// 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.Logger)
|
||||||
r.Use(middleware.Recoverer)
|
r.Use(middleware.Recoverer)
|
||||||
r.Use(middleware.CORS(cfg.AllowedOrigins))
|
r.Use(middleware.CORS(cfg.AllowedOrigins))
|
||||||
|
r.Use(middleware.SecurityHeaders())
|
||||||
r.Use(middleware.RateLimit)
|
r.Use(middleware.RateLimit)
|
||||||
|
|
||||||
// Health check
|
// Health check
|
||||||
@@ -571,6 +572,11 @@ func listFilesHandler(w http.ResponseWriter, r *http.Request, db *database.DB) {
|
|||||||
if path == "" {
|
if path == "" {
|
||||||
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")
|
q := r.URL.Query().Get("q")
|
||||||
page := 1
|
page := 1
|
||||||
pageSize := 100
|
pageSize := 100
|
||||||
@@ -1871,6 +1877,11 @@ func createOrgFileHandler(w http.ResponseWriter, r *http.Request, db *database.D
|
|||||||
if parentPath == "" {
|
if parentPath == "" {
|
||||||
parentPath = "/"
|
parentPath = "/"
|
||||||
}
|
}
|
||||||
|
parentPath, err = sanitizePath(parentPath)
|
||||||
|
if err != nil {
|
||||||
|
errors.WriteError(w, errors.CodeInvalidArgument, "Invalid path", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
var file multipart.File
|
var file multipart.File
|
||||||
var header *multipart.FileHeader
|
var header *multipart.FileHeader
|
||||||
file, header, err = r.FormFile("file")
|
file, header, err = r.FormFile("file")
|
||||||
@@ -1972,6 +1983,13 @@ func deleteOrgFileHandler(w http.ResponseWriter, r *http.Request, db *database.D
|
|||||||
return
|
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
|
// Get or create user's WebDAV client and delete from Nextcloud
|
||||||
storageClient, err := getUserWebDAVClient(r.Context(), db, userID, cfg.NextcloudURL, cfg.NextcloudUser, cfg.NextcloudPass)
|
storageClient, err := getUserWebDAVClient(r.Context(), db, userID, cfg.NextcloudURL, cfg.NextcloudUser, cfg.NextcloudPass)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -2022,6 +2040,18 @@ func moveOrgFileHandler(w http.ResponseWriter, r *http.Request, db *database.DB,
|
|||||||
return
|
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
|
// Get source file details before moving
|
||||||
sourceFiles, err := db.GetOrgFiles(r.Context(), orgID, userID, "/", "", 0, 1000)
|
sourceFiles, err := db.GetOrgFiles(r.Context(), orgID, userID, "/", "", 0, 1000)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -2134,6 +2164,11 @@ func createUserFileHandler(w http.ResponseWriter, r *http.Request, db *database.
|
|||||||
if parentPath == "" {
|
if parentPath == "" {
|
||||||
parentPath = "/"
|
parentPath = "/"
|
||||||
}
|
}
|
||||||
|
parentPath, err = sanitizePath(parentPath)
|
||||||
|
if err != nil {
|
||||||
|
errors.WriteError(w, errors.CodeInvalidArgument, "Invalid path", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
var file multipart.File
|
var file multipart.File
|
||||||
var header *multipart.FileHeader
|
var header *multipart.FileHeader
|
||||||
file, header, err = r.FormFile("file")
|
file, header, err = r.FormFile("file")
|
||||||
@@ -2242,6 +2277,18 @@ func moveUserFileHandler(w http.ResponseWriter, r *http.Request, db *database.DB
|
|||||||
return
|
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
|
// Get source file details before moving
|
||||||
sourceFile, err := db.GetUserFileByPath(r.Context(), userID, req.SourcePath)
|
sourceFile, err := db.GetUserFileByPath(r.Context(), userID, req.SourcePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -2339,6 +2386,13 @@ func deleteUserFileHandler(w http.ResponseWriter, r *http.Request, db *database.
|
|||||||
return
|
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
|
// Get or create user's WebDAV client and delete from Nextcloud
|
||||||
storageClient, err := getUserWebDAVClient(r.Context(), db, userID, cfg.NextcloudURL, cfg.NextcloudUser, cfg.NextcloudPass)
|
storageClient, err := getUserWebDAVClient(r.Context(), db, userID, cfg.NextcloudURL, cfg.NextcloudUser, cfg.NextcloudPass)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -25,6 +25,27 @@ var RequestID = middleware.RequestID
|
|||||||
var Logger = middleware.Logger
|
var Logger = middleware.Logger
|
||||||
var Recoverer = middleware.Recoverer
|
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
|
// CORS middleware - accepts allowedOrigins comma-separated string
|
||||||
func CORS(allowedOrigins string) func(http.Handler) http.Handler {
|
func CORS(allowedOrigins string) func(http.Handler) http.Handler {
|
||||||
allowedList, allowAll := compileAllowedOrigins(allowedOrigins)
|
allowedList, allowAll := compileAllowedOrigins(allowedOrigins)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"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))
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"path"
|
"path"
|
||||||
@@ -24,7 +25,7 @@ type WebDAVClient struct {
|
|||||||
// NewWebDAVClient returns nil if no Nextcloud URL configured
|
// NewWebDAVClient returns nil if no Nextcloud URL configured
|
||||||
func NewWebDAVClient(cfg *config.Config) *WebDAVClient {
|
func NewWebDAVClient(cfg *config.Config) *WebDAVClient {
|
||||||
if cfg == nil || strings.TrimSpace(cfg.NextcloudURL) == "" {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
u := strings.TrimRight(cfg.NextcloudURL, "/")
|
u := strings.TrimRight(cfg.NextcloudURL, "/")
|
||||||
@@ -32,7 +33,7 @@ func NewWebDAVClient(cfg *config.Config) *WebDAVClient {
|
|||||||
if base == "" {
|
if base == "" {
|
||||||
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{
|
return &WebDAVClient{
|
||||||
baseURL: u,
|
baseURL: u,
|
||||||
user: cfg.NextcloudUser,
|
user: cfg.NextcloudUser,
|
||||||
|
|||||||
Reference in New Issue
Block a user