Skip to content

Commit 8c50603

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 8c50603

File tree

1 file changed

+42
-6
lines changed

1 file changed

+42
-6
lines changed

internal/handler/file.go

Lines changed: 42 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,10 +502,46 @@ 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 parameter must be constructed from a validated absolute filesystem path
507+
// (e.g., absPath + ".gz") to ensure it remains within the root directory.
508+
func (h *FileHandler) loadSidecar(path string) []byte {
509+
// Resolve symlinks to get the canonical path.
510+
// This prevents symlink escape attacks where a sidecar could point outside root.
511+
realPath, err := filepath.EvalSymlinks(path)
508512
if err != nil {
513+
// File doesn't exist or can't be resolved — return nil.
514+
if os.IsNotExist(err) {
515+
return nil
516+
}
517+
// Other errors (permission denied, etc.) — treat as inaccessible.
518+
return nil
519+
}
520+
521+
// Resolve the root directory to its canonical path for comparison.
522+
// This is important on platforms like macOS where /tmp → /private/tmp.
523+
realRoot := h.absRoot
524+
if r, err := filepath.EvalSymlinks(h.absRoot); err == nil {
525+
realRoot = r
526+
}
527+
528+
// Ensure the resolved sidecar path is still within the root directory.
529+
// Add a trailing separator to prevent prefix collisions like "/root" matching "/rootsuffix".
530+
rootWithSep := realRoot
531+
if !strings.HasSuffix(rootWithSep, string(filepath.Separator)) {
532+
rootWithSep += string(filepath.Separator)
533+
}
534+
535+
// Reject if the sidecar path escapes the root directory.
536+
if realPath != realRoot && !strings.HasPrefix(realPath, rootWithSep) {
537+
// Sidecar path escapes the root — reject it.
538+
return nil
539+
}
540+
541+
// Path is validated and safe — read the file.
542+
data, err := os.ReadFile(realPath)
543+
if err != nil {
544+
// File doesn't exist or can't be read — return nil.
509545
return nil
510546
}
511547
return data

0 commit comments

Comments
 (0)