Skip to content
Merged
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 53 additions & 6 deletions internal/handler/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -297,9 +297,9 @@
// compressible and large enough to benefit from compression.
if h.cfg.Compression.Enabled && h.cfg.Compression.Precompressed &&
compress.IsCompressible(ct) && len(data) >= h.cfg.Compression.MinSize {
cached.GzipData = loadSidecar(absPath + ".gz")
cached.BrData = loadSidecar(absPath + ".br")
cached.ZstdData = loadSidecar(absPath + ".zst")
cached.GzipData = h.loadSidecar(absPath + ".gz")
cached.BrData = h.loadSidecar(absPath + ".br")
cached.ZstdData = h.loadSidecar(absPath + ".zst")
}

// Generate on-the-fly gzip if no sidecar and content is compressible.
Expand Down Expand Up @@ -501,11 +501,58 @@
return "application/octet-stream"
}

// validateSidecarPath validates that a sidecar file path is within the root directory.
// It resolves symlinks to prevent escape attacks and ensures the canonical path
// remains within the root. Returns the validated path or an error if validation fails.
// This function is designed to be recognized by static analyzers as a path sanitizer.
func (h *FileHandler) validateSidecarPath(sidecarPath string) (string, error) {
// Resolve symlinks to get the canonical path.
// This prevents symlink escape attacks where a sidecar could point outside root.
realPath, err := filepath.EvalSymlinks(sidecarPath)
if err != nil {
// File doesn't exist or can't be resolved — return error.
return "", err
}

// Resolve the root directory to its canonical path for comparison.
// This is important on platforms like macOS where /tmp → /private/tmp.
realRoot := h.absRoot
if r, err := filepath.EvalSymlinks(h.absRoot); err == nil {
realRoot = r
}

// Ensure the resolved sidecar path is still within the root directory.
// Add a trailing separator to prevent prefix collisions like "/root" matching "/rootsuffix".
rootWithSep := realRoot
if !strings.HasSuffix(rootWithSep, string(filepath.Separator)) {
rootWithSep += string(filepath.Separator)
}

// Reject if the sidecar path escapes the root directory.
if realPath != realRoot && !strings.HasPrefix(realPath, rootWithSep) {
// Sidecar path escapes the root — reject it.
return "", fmt.Errorf("sidecar path escapes root directory")
}

return realPath, nil
}

// loadSidecar attempts to read a pre-compressed sidecar file.
// Returns nil if the sidecar does not exist or cannot be read.
func loadSidecar(path string) []byte {
data, err := os.ReadFile(path)
// Returns nil if the sidecar does not exist, cannot be read, or fails validation.
// The path parameter must be constructed from a validated absolute filesystem path
// (e.g., absPath + ".gz") to ensure it remains within the root directory.
func (h *FileHandler) loadSidecar(path string) []byte {
// Validate the sidecar path to prevent path traversal attacks.
validatedPath, err := h.validateSidecarPath(path)
if err != nil {
// Validation failed (symlink escape, doesn't exist, etc.) — return nil.
return nil
}

// Path is validated and safe — read the file.
data, err := os.ReadFile(validatedPath)
Comment thread Dismissed
if err != nil {
// File doesn't exist or can't be read — return nil.
return nil
}
return data
Expand Down
Loading