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 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), "\"", """)
},
"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:
- Strict Path Validation: Validate all file paths with absolute paths and prevent access outside allowed directories
- Input Sanitization: Proper escaping and sanitization of filenames and path parameters
- Security Headers: Appropriate HTTP header configuration for browser-level attack prevention
- Upload Restrictions: Proper limitations on file size and file types
- Logging: Appropriate recording of security events and audit trails
- 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
- GitHub Repository: https://github.com/yuis-ice/go-filemanager
- OWASP Path Traversal: https://owasp.org/www-community/attacks/Path_Traversal
- Go Security Guidelines: https://golang.org/doc/security
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.