0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Building Secure File Manager with Go Language

Posted at

Introduction

When developing web-based file managers, security is one of the most critical elements. Applications involving direct file system access face various security risks including path traversal attacks, unauthorized file access, and XSS attacks.

This article provides detailed explanations of specific techniques and implementation methods for building secure file managers using Go language. Using code examples from an actual open source project, I'll introduce practical security measures.

Security Architecture

Security Threat Analysis

Path Traversal Attacks

Path traversal attacks use relative path strings like ../ to access files outside the application's intended directory. In web-based file managers, the following attacks are possible:

GET /download?path=../../../etc/passwd
GET /preview?file=....//....//etc/hosts
POST /upload?destination=../../root/.ssh/

File Upload Attacks

Malicious file uploads can lead to arbitrary code execution on the server. Particularly when script files are uploaded to web server executable directories, this becomes a serious security risk.

XSS Attacks

When file names or directory names contain malicious JavaScript code, these scripts may execute when displayed in HTML.

Implementing Secure Path Processing

Absolute Path Validation System

The most effective method to prevent path traversal attacks is converting all file paths to absolute paths and verifying they're contained within the allowed root directory.

package main

import (
    "fmt"
    "path/filepath"
    "strings"
)

// Secure path validation function
func validatePath(rootDir, requestedPath string) (string, error) {
    // Convert root directory to absolute path
    absRootDir, err := filepath.Abs(rootDir)
    if err != nil {
        return "", fmt.Errorf("failed to get absolute path of root directory: %v", err)
    }
    
    // Join requested path
    fullPath := filepath.Join(absRootDir, requestedPath)
    
    // Normalize path
    cleanPath := filepath.Clean(fullPath)
    
    // Convert to absolute path
    absPath, err := filepath.Abs(cleanPath)
    if err != nil {
        return "", fmt.Errorf("absolute path conversion failed: %v", err)
    }
    
    // Check if contained within root directory
    if !strings.HasPrefix(absPath, absRootDir) {
        return "", fmt.Errorf("unauthorized path access: %s", requestedPath)
    }
    
    return absPath, nil
}

// Usage example
func exampleUsage() {
    rootDir := "/home/filemanager"
    
    // Valid path
    validPath, err := validatePath(rootDir, "documents/file.txt")
    if err != nil {
        fmt.Printf("Error: %v\n", err)
    } else {
        fmt.Printf("Valid path: %s\n", validPath)
    }
    
    // Malicious path
    invalidPath, err := validatePath(rootDir, "../../../etc/passwd")
    if err != nil {
        fmt.Printf("Attack detected: %v\n", err)
    } else {
        fmt.Printf("Path: %s\n", invalidPath)
    }
}

File Operation Handler Implementation

Implementing HTTP handlers for secure file operations.

package main

import (
    "fmt"
    "html"
    "io"
    "net/http"
    "os"
    "path/filepath"
    "strings"
)

type FileManager struct {
    RootDir string
    MaxUploadSize int64
}

// Download handler
func (fm *FileManager) downloadHandler(w http.ResponseWriter, r *http.Request) {
    // Get path parameter
    requestedPath := r.URL.Query().Get("path")
    if requestedPath == "" {
        http.Error(w, "Path not specified", http.StatusBadRequest)
        return
    }
    
    // Path validation
    safePath, err := validatePath(fm.RootDir, requestedPath)
    if err != nil {
        http.Error(w, "Invalid path", http.StatusForbidden)
        return
    }
    
    // File existence check
    fileInfo, err := os.Stat(safePath)
    if err != nil {
        http.Error(w, "File not found", http.StatusNotFound)
        return
    }
    
    // Error if directory
    if fileInfo.IsDir() {
        http.Error(w, "Cannot download directory", http.StatusBadRequest)
        return
    }
    
    // Open file
    file, err := os.Open(safePath)
    if err != nil {
        http.Error(w, "File open error", http.StatusInternalServerError)
        return
    }
    defer file.Close()
    
    // Set response headers
    filename := filepath.Base(safePath)
    w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
    w.Header().Set("Content-Type", "application/octet-stream")
    w.Header().Set("Content-Length", fmt.Sprintf("%d", fileInfo.Size()))
    
    // Copy file content
    _, err = io.Copy(w, file)
    if err != nil {
        // Log (use appropriate logging library in actual implementation)
        fmt.Printf("File transmission error: %v\n", err)
    }
}

// Upload handler
func (fm *FileManager) uploadHandler(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        http.Error(w, "Only POST method allowed", http.StatusMethodNotAllowed)
        return
    }
    
    // File size limit
    r.Body = http.MaxBytesReader(w, r.Body, fm.MaxUploadSize)
    
    // Parse multipart form
    err := r.ParseMultipartForm(fm.MaxUploadSize)
    if err != nil {
        http.Error(w, "File size too large", http.StatusBadRequest)
        return
    }
    
    // Validate upload destination path
    uploadDir := r.FormValue("path")
    safePath, err := validatePath(fm.RootDir, uploadDir)
    if err != nil {
        http.Error(w, "Invalid upload path", http.StatusForbidden)
        return
    }
    
    // Check directory existence
    if _, err := os.Stat(safePath); os.IsNotExist(err) {
        http.Error(w, "Upload destination directory does not exist", http.StatusNotFound)
        return
    }
    
    // Get upload file
    file, header, err := r.FormFile("file")
    if err != nil {
        http.Error(w, "File retrieval error", http.StatusBadRequest)
        return
    }
    defer file.Close()
    
    // Filename validation and sanitization
    filename := sanitizeFilename(header.Filename)
    if filename == "" {
        http.Error(w, "Invalid filename", http.StatusBadRequest)
        return
    }
    
    // Build destination path
    destPath := filepath.Join(safePath, filename)
    
    // Create file
    destFile, err := os.Create(destPath)
    if err != nil {
        http.Error(w, "File creation error", http.StatusInternalServerError)
        return
    }
    defer destFile.Close()
    
    // Copy file content
    _, err = io.Copy(destFile, file)
    if err != nil {
        http.Error(w, "File save error", http.StatusInternalServerError)
        return
    }
    
    // Success response
    w.Header().Set("Content-Type", "application/json")
    fmt.Fprintf(w, `{"success": true, "message": "File upload completed"}`)
}

// Filename sanitization
func sanitizeFilename(filename string) string {
    // Remove dangerous characters
    dangerous := []string{"..", "/", "\\", ":", "*", "?", "\"", "<", ">", "|"}
    sanitized := filename
    
    for _, char := range dangerous {
        sanitized = strings.ReplaceAll(sanitized, char, "_")
    }
    
    // Trim whitespace
    sanitized = strings.TrimSpace(sanitized)
    
    // Maximum length limit
    if len(sanitized) > 255 {
        sanitized = sanitized[:255]
    }
    
    return sanitized
}

XSS Attack Prevention

Implementing XSS attack prevention measures when displaying file information in HTML templates.

package main

import (
    "html"
    "html/template"
    "strings"
)

// Template functions for XSS prevention
func createTemplateFunctions() template.FuncMap {
    return template.FuncMap{
        "escapeHTML": func(s string) string {
            return html.EscapeString(s)
        },
        "sanitizeAttr": func(s string) string {
            // Convert to safe string for HTML attribute values
            return strings.ReplaceAll(html.EscapeString(s), "\"", "&quot;")
        },
        "truncate": func(s string, length int) string {
            if len(s) <= length {
                return s
            }
            return s[:length] + "..."
        },
    }
}

// Template usage example
const templateHTML = `
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>{{.Title | escapeHTML}}</title>
</head>
<body>
    <h1>File List</h1>
    <table>
        <thead>
            <tr>
                <th>Filename</th>
                <th>Size</th>
                <th>Actions</th>
            </tr>
        </thead>
        <tbody>
            {{range .Files}}
            <tr>
                <td>{{.Name | escapeHTML | truncate 50}}</td>
                <td>{{.Size}}</td>
                <td>
                    <a href="/download?path={{.Path | sanitizeAttr}}">Download</a>
                    <a href="/preview?file={{.Path | sanitizeAttr}}">Preview</a>
                </td>
            </tr>
            {{end}}
        </tbody>
    </table>
</body>
</html>
`

Security Header Configuration

Setting security headers in HTTP responses to prevent browser-level attacks.

package main

import (
    "net/http"
)

// Security middleware
func securityMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // XSS Protection
        w.Header().Set("X-XSS-Protection", "1; mode=block")
        
        // Prevent content type sniffing
        w.Header().Set("X-Content-Type-Options", "nosniff")
        
        // Prevent frame embedding
        w.Header().Set("X-Frame-Options", "DENY")
        
        // Force HTTPS (if needed)
        w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
        
        // CSP configuration
        w.Header().Set("Content-Security-Policy", 
            "default-src 'self'; "+
            "script-src 'self' 'unsafe-inline'; "+
            "style-src 'self' 'unsafe-inline'; "+
            "img-src 'self' data:; "+
            "object-src 'none'")
        
        // Referrer policy
        w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
        
        next.ServeHTTP(w, r)
    })
}

// Server configuration
func setupSecureServer() *http.Server {
    mux := http.NewServeMux()
    
    // Register file manager handlers
    fm := &FileManager{
        RootDir: "/home/filemanager",
        MaxUploadSize: 100 * 1024 * 1024, // 100MB
    }
    
    mux.HandleFunc("/", fm.indexHandler)
    mux.HandleFunc("/download", fm.downloadHandler)
    mux.HandleFunc("/upload", fm.uploadHandler)
    
    // Apply security middleware
    secureHandler := securityMiddleware(mux)
    
    return &http.Server{
        Addr:    ":8086",
        Handler: secureHandler,
    }
}

Logging and Auditing

Implementing appropriate logging functionality for security incident detection and investigation.

package main

import (
    "fmt"
    "log"
    "net/http"
    "strings"
    "time"
)

// Security event logging
type SecurityLogger struct {
    logger *log.Logger
}

func (sl *SecurityLogger) LogSecurityEvent(eventType, clientIP, message string) {
    timestamp := time.Now().Format(time.RFC3339)
    sl.logger.Printf("[SECURITY] %s | %s | %s | %s", timestamp, eventType, clientIP, message)
}

// Request logging middleware
func loggingMiddleware(logger *SecurityLogger) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            start := time.Now()
            
            // Get client IP
            clientIP := r.Header.Get("X-Real-IP")
            if clientIP == "" {
                clientIP = r.Header.Get("X-Forwarded-For")
            }
            if clientIP == "" {
                clientIP = r.RemoteAddr
            }
            
            // Check suspicious path patterns
            suspiciousPatterns := []string{"../", "..\\", "%2e%2e", "etc/passwd", ".ssh"}
            for _, pattern := range suspiciousPatterns {
                if strings.Contains(r.URL.Path, pattern) || strings.Contains(r.URL.RawQuery, pattern) {
                    logger.LogSecurityEvent("SUSPICIOUS_PATH", clientIP, 
                        fmt.Sprintf("Suspicious path access: %s?%s", r.URL.Path, r.URL.RawQuery))
                }
            }
            
            // Wrap response writer to record status code
            wrapped := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
            
            next.ServeHTTP(wrapped, r)
            
            duration := time.Since(start)
            
            // Record access log
            log.Printf("%s %s %s %d %v", 
                clientIP, r.Method, r.URL.String(), wrapped.statusCode, duration)
            
            // Record error responses in security log
            if wrapped.statusCode >= 400 {
                logger.LogSecurityEvent("HTTP_ERROR", clientIP,
                    fmt.Sprintf("%s %s -> %d", r.Method, r.URL.String(), wrapped.statusCode))
            }
        })
    }
}

type responseWriter struct {
    http.ResponseWriter
    statusCode int
}

func (rw *responseWriter) WriteHeader(code int) {
    rw.statusCode = code
    rw.ResponseWriter.WriteHeader(code)
}

Security Testing Implementation

Implementing test code to verify that developed security features work correctly.

package main

import (
    "net/http"
    "net/http/httptest"
    "strings"
    "testing"
)

func TestPathValidation(t *testing.T) {
    tests := []struct {
        name        string
        rootDir     string
        requestPath string
        expectError bool
    }{
        {
            name:        "Valid path",
            rootDir:     "/home/test",
            requestPath: "documents/file.txt",
            expectError: false,
        },
        {
            name:        "Path traversal attack",
            rootDir:     "/home/test",
            requestPath: "../../../etc/passwd",
            expectError: true,
        },
        {
            name:        "Relative path attack",
            rootDir:     "/home/test",
            requestPath: "../../root/.ssh/",
            expectError: true,
        },
        {
            name:        "URL encoded attack",
            rootDir:     "/home/test",
            requestPath: "..%2f..%2f..%2fetc%2fpasswd",
            expectError: true,
        },
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            _, err := validatePath(tt.rootDir, tt.requestPath)
            if (err != nil) != tt.expectError {
                t.Errorf("validatePath() error = %v, expectError %v", err, tt.expectError)
            }
        })
    }
}

func TestDownloadSecurity(t *testing.T) {
    fm := &FileManager{
        RootDir: "/tmp/test",
        MaxUploadSize: 1024 * 1024,
    }
    
    // Test malicious request
    req := httptest.NewRequest("GET", "/download?path=../../../etc/passwd", nil)
    w := httptest.NewRecorder()
    
    fm.downloadHandler(w, req)
    
    if w.Code != http.StatusForbidden {
        t.Errorf("Expected status code %d, actual status code %d", http.StatusForbidden, w.Code)
    }
}

func TestXSSPrevention(t *testing.T) {
    maliciousFilename := "<script>alert('xss')</script>test.txt"
    sanitized := sanitizeFilename(maliciousFilename)
    
    if strings.Contains(sanitized, "<script>") {
        t.Errorf("XSS script not sanitized: %s", sanitized)
    }
}

Summary

Key points for building secure web-based file managers using Go language:

  1. Strict Path Validation: Validate all file paths with absolute paths and prevent access outside allowed directories
  2. Input Sanitization: Proper escaping and sanitization of filenames and path parameters
  3. Security Headers: Appropriate HTTP header configuration for browser-level attack prevention
  4. Upload Restrictions: Proper limitations on file size and file types
  5. Logging: Appropriate recording of security events and audit trails
  6. Continuous Testing: Automated test implementation for security feature verification

By properly implementing these measures, you can build highly secure file managers. Remember that security is not a one-time implementation but requires continuous review and improvement.

Reference Links


Security Notice: The content introduced in this article are examples of general security measures. For actual production environments, additional security audits and expert reviews are recommended.

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?