Skip to content

Commit d63c759

Browse files
committed
fix(security): validate sidecar paths to prevent path injection attacks
Fixes CodeQL path-injection warning in loadSidecar function. The sidecar file paths (.gz, .br, .zst extensions) are now validated to ensure they remain within the root directory, preventing symlink escape attacks. - Convert loadSidecar to a method on FileHandler for access to absRoot - Resolve symlinks in both the sidecar path and root directory - Validate sidecar path is within root before reading - Log rejected paths for security auditing
1 parent 4d8bd9f commit d63c759

File tree

1 file changed

+41
-6
lines changed

1 file changed

+41
-6
lines changed

internal/handler/file.go

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -297,9 +297,9 @@ func (h *FileHandler) serveFromDisk(ctx *fasthttp.RequestCtx, absPath, urlPath s
297297
// compressible and large enough to benefit from compression.
298298
if h.cfg.Compression.Enabled && h.cfg.Compression.Precompressed &&
299299
compress.IsCompressible(ct) && len(data) >= h.cfg.Compression.MinSize {
300-
cached.GzipData = loadSidecar(absPath + ".gz")
301-
cached.BrData = loadSidecar(absPath + ".br")
302-
cached.ZstdData = loadSidecar(absPath + ".zst")
300+
cached.GzipData = h.loadSidecar(absPath + ".gz")
301+
cached.BrData = h.loadSidecar(absPath + ".br")
302+
cached.ZstdData = h.loadSidecar(absPath + ".zst")
303303
}
304304

305305
// Generate on-the-fly gzip if no sidecar and content is compressible.
@@ -502,9 +502,44 @@ func detectContentType(path string, data []byte) string {
502502
}
503503

504504
// loadSidecar attempts to read a pre-compressed sidecar file.
505-
// Returns nil if the sidecar does not exist or cannot be read.
506-
func loadSidecar(path string) []byte {
507-
data, err := os.ReadFile(path)
505+
// Returns nil if the sidecar does not exist, cannot be read, or fails validation.
506+
// The path must be a validated absolute filesystem path; this function validates
507+
// that the sidecar file is still within the expected root directory to prevent
508+
// path injection attacks (e.g., symlink escape via sidecar extension).
509+
func (h *FileHandler) loadSidecar(path string) []byte {
510+
// Validate that the sidecar path is still within the root directory.
511+
// This prevents symlink escape attacks where a sidecar file could point
512+
// outside the intended directory tree.
513+
realPath, err := filepath.EvalSymlinks(path)
514+
if err != nil {
515+
// File doesn't exist or can't be resolved — safe to return nil.
516+
if os.IsNotExist(err) {
517+
return nil
518+
}
519+
// Other errors (permission denied, etc.) — treat as inaccessible.
520+
return nil
521+
}
522+
523+
// Resolve the root directory to its canonical path for comparison.
524+
// This is important on platforms like macOS where /tmp → /private/tmp.
525+
realRoot := h.absRoot
526+
if r, err := filepath.EvalSymlinks(h.absRoot); err == nil {
527+
realRoot = r
528+
}
529+
530+
// Ensure the resolved sidecar path is still within the root directory.
531+
if !strings.HasSuffix(realRoot, string(filepath.Separator)) {
532+
realRoot += string(filepath.Separator)
533+
}
534+
535+
if realPath != realRoot && !strings.HasPrefix(realPath, realRoot) {
536+
// Sidecar path escapes the root — reject it.
537+
log.Printf("handler: sidecar path escapes root: %q", path)
538+
return nil
539+
}
540+
541+
// Path is safe — read the file.
542+
data, err := os.ReadFile(realPath)
508543
if err != nil {
509544
return nil
510545
}

0 commit comments

Comments
 (0)