From 4fc0df5a01234c88f9cf332b07cc27b221d415b8 Mon Sep 17 00:00:00 2001 From: vmfunc Date: Mon, 8 Jun 2026 17:22:03 -0700 Subject: [PATCH] fix(templates): guard tar extraction against path traversal The nuclei-templates tarball is fetched over the network and its entry names flowed directly into os.Mkdir/os.Create, so a malicious or compromised archive could write outside the extraction directory ("Zip Slip", CWE-22). Resolve each entry against the working directory and reject any path that escapes it before touching the filesystem. CodeQL flagged this as a high-severity alert on the lines this branch already touched. gosec's G305 fires on filepath.Join with archive data regardless of the traversal guard, so it's excluded with a note. --- .golangci.yml | 2 ++ internal/nuclei/templates/templates.go | 20 ++++++++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index a699b97..b76f305 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -78,6 +78,8 @@ linters: - G107 # pentesting tool -- variable URLs are the whole point - G110 # nuclei template decompression, acceptable context - G304 # sif reads user-supplied wordlist paths -- intentional + - G305 # tar extraction is traversal-guarded (HasPrefix on the + # cleaned target); gosec flags filepath.Join regardless exclusions: rules: diff --git a/internal/nuclei/templates/templates.go b/internal/nuclei/templates/templates.go index 80f117b..fbbc403 100644 --- a/internal/nuclei/templates/templates.go +++ b/internal/nuclei/templates/templates.go @@ -21,6 +21,8 @@ import ( "io" "net/http" "os" + "path/filepath" + "strings" "github.com/charmbracelet/log" ) @@ -61,6 +63,12 @@ func Install(logger *log.Logger) error { data := tar.NewReader(tarball) + dest, err := os.Getwd() + if err != nil { + return err + } + cleanDest := filepath.Clean(dest) + for { header, err := data.Next() if errors.Is(err, io.EOF) { @@ -70,17 +78,25 @@ func Install(logger *log.Logger) error { return err } + // guard against path traversal ("Zip Slip"): the resolved path must + // stay within the extraction directory before any filesystem op. + target := filepath.Join(cleanDest, header.Name) + if !strings.HasPrefix(target, cleanDest+string(os.PathSeparator)) { + return fmt.Errorf("invalid archive entry %q: escapes extraction directory", header.Name) + } + switch header.Typeflag { case tar.TypeDir: - if err := os.Mkdir(header.Name, 0o750); err != nil { + if err := os.Mkdir(target, 0o750); err != nil { return err } case tar.TypeReg: - file, err := os.Create(header.Name) + file, err := os.Create(target) if err != nil { return err } if _, err := io.Copy(file, data); err != nil { + file.Close() return err } file.Close()