diff --git a/internal/httpx/httpx.go b/internal/httpx/httpx.go index 4ea20bcc..fd4ca092 100644 --- a/internal/httpx/httpx.go +++ b/internal/httpx/httpx.go @@ -68,6 +68,11 @@ const ( // conn, so we cap the read and let the conn be discarded instead. const drainCap = 16 << 10 +// MaxBodySize is the shared ceiling on how much of a response body the scanners +// read into memory, so a hostile or accidental multi-gigabyte response can't +// exhaust the process. +const MaxBodySize = 5 << 20 + // Options carries the runtime knobs that apply to every outbound request. // RateLimit is requests/sec (0 = unlimited); Headers are "Key: Value" strings. type Options struct { @@ -213,6 +218,13 @@ func DrainClose(resp *http.Response) { resp.Body.Close() } +// ReadCappedBody reads resp.Body up to MaxBodySize, the shared cap every scanner +// uses so one runaway response can't exhaust memory. It does not close the body; +// pair it with DrainClose or an explicit Close. +func ReadCappedBody(resp *http.Response) ([]byte, error) { + return io.ReadAll(io.LimitReader(resp.Body, MaxBodySize)) +} + // parseHeaders splits each "Key: Value" entry on the first ": ". Entries // without the separator are rejected so a typo fails loud instead of silently. // The returned map is always non-nil so callers can range it unconditionally. diff --git a/internal/modules/executor.go b/internal/modules/executor.go index f969581a..40c1d963 100644 --- a/internal/modules/executor.go +++ b/internal/modules/executor.go @@ -26,11 +26,9 @@ import ( "time" "github.com/tidwall/gjson" + "github.com/vmfunc/sif/internal/httpx" ) -// MaxBodySize limits response body to prevent memory exhaustion. -const MaxBodySize = 5 * 1024 * 1024 - // ErrUnsupportedModuleType signals an executor for a module type that is not // yet implemented. Returning it (rather than an empty result) keeps callers // from mistaking "not implemented" for "scanned, found nothing". @@ -297,7 +295,7 @@ func executeHTTPRequest(ctx context.Context, client *http.Client, r *httpRequest defer resp.Body.Close() // Read body with limit - respBody, err := io.ReadAll(io.LimitReader(resp.Body, MaxBodySize)) + respBody, err := httpx.ReadCappedBody(resp) if err != nil { return Finding{}, false } diff --git a/internal/modules/loader.go b/internal/modules/loader.go index 42906a6f..342e0af4 100644 --- a/internal/modules/loader.go +++ b/internal/modules/loader.go @@ -16,10 +16,10 @@ import ( "fmt" "os" "path/filepath" - "runtime" "github.com/charmbracelet/log" "github.com/vmfunc/sif/internal/output" + "github.com/vmfunc/sif/internal/sifpath" ) // Loader handles module discovery and loading. @@ -33,11 +33,6 @@ type Loader struct { // It automatically detects the built-in modules directory and sets up // the user modules directory based on the operating system. func NewLoader() (*Loader, error) { - home, err := os.UserHomeDir() - if err != nil { - return nil, fmt.Errorf("get home dir: %w", err) - } - // Find built-in modules relative to executable execPath, err := os.Executable() if err != nil { @@ -50,13 +45,10 @@ func NewLoader() (*Loader, error) { builtinDir = "modules" } - // User modules directory based on OS - var userDir string - switch runtime.GOOS { - case "windows": - userDir = filepath.Join(home, "AppData", "Local", "sif", "modules") - default: - userDir = filepath.Join(home, ".config", "sif", "modules") + // User modules directory (can override built-ins) + userDir, err := sifpath.UserSubdir("modules") + if err != nil { + return nil, fmt.Errorf("resolve user modules dir: %w", err) } return &Loader{ diff --git a/internal/scan/frameworks/custom.go b/internal/scan/frameworks/custom.go index c9693c79..104bc6ed 100644 --- a/internal/scan/frameworks/custom.go +++ b/internal/scan/frameworks/custom.go @@ -26,11 +26,11 @@ import ( "os" "path/filepath" "regexp" - "runtime" "strings" charmlog "github.com/charmbracelet/log" "github.com/vmfunc/sif/internal/output" + "github.com/vmfunc/sif/internal/sifpath" "gopkg.in/yaml.v3" ) @@ -129,14 +129,7 @@ func parseCustomDetector(path string) (Detector, error) { // customSignaturesDir is the per-user directory that holds yaml-defined // detectors, alongside the user modules directory. func customSignaturesDir() (string, error) { - home, err := os.UserHomeDir() - if err != nil { - return "", err - } - if runtime.GOOS == "windows" { - return filepath.Join(home, "AppData", "Local", "sif", "signatures"), nil - } - return filepath.Join(home, ".config", "sif", "signatures"), nil + return sifpath.UserSubdir("signatures") } // loadCustomDetectors registers every signature file under the user directory. diff --git a/internal/scan/frameworks/detect.go b/internal/scan/frameworks/detect.go index bf2dd2a0..763bbb2c 100644 --- a/internal/scan/frameworks/detect.go +++ b/internal/scan/frameworks/detect.go @@ -15,7 +15,6 @@ package frameworks import ( "context" "fmt" - "io" "net/http" "strings" "sync" @@ -30,9 +29,6 @@ import ( // detectionThreshold is the minimum confidence for a detection to be reported. const detectionThreshold = 0.5 -// maxBodySize limits response body to prevent memory exhaustion. -const maxBodySize = 5 * 1024 * 1024 - // detectionResult holds the result from a single detector. type detectionResult struct { name string @@ -68,7 +64,7 @@ func DetectFramework(url string, timeout time.Duration, logdir string) (*Framewo } defer resp.Body.Close() - body, err := io.ReadAll(io.LimitReader(resp.Body, maxBodySize)) + body, err := httpx.ReadCappedBody(resp) if err != nil { spin.Stop() return nil, err diff --git a/internal/sifpath/sifpath.go b/internal/sifpath/sifpath.go new file mode 100644 index 00000000..dee5e9c2 --- /dev/null +++ b/internal/sifpath/sifpath.go @@ -0,0 +1,36 @@ +/* +·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· +: : +: █▀ █ █▀▀ · Blazing-fast pentesting suite : +: ▄█ █ █▀ · BSD 3-Clause License : +: : +: (c) 2022-2026 vmfunc, xyzeva, : +: lunchcat alumni & contributors : +: : +·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· +*/ + +// Package sifpath resolves the per-user sif directories so every subsystem +// agrees on where user-supplied files live. +package sifpath + +import ( + "os" + "path/filepath" + "runtime" +) + +// UserSubdir returns the per-user sif configuration subdirectory for name (for +// example "modules" or "signatures"). It preserves sif's historical layout: +// ~/.config/sif/ on unix-like systems and %LOCALAPPDATA%\sif\ on +// windows. +func UserSubdir(name string) (string, error) { + home, err := os.UserHomeDir() + if err != nil { + return "", err + } + if runtime.GOOS == "windows" { + return filepath.Join(home, "AppData", "Local", "sif", name), nil + } + return filepath.Join(home, ".config", "sif", name), nil +} diff --git a/internal/sifpath/sifpath_test.go b/internal/sifpath/sifpath_test.go new file mode 100644 index 00000000..52c8e829 --- /dev/null +++ b/internal/sifpath/sifpath_test.go @@ -0,0 +1,48 @@ +/* +·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· +: : +: █▀ █ █▀▀ · Blazing-fast pentesting suite : +: ▄█ █ █▀ · BSD 3-Clause License : +: : +: (c) 2022-2026 vmfunc, xyzeva, : +: lunchcat alumni & contributors : +: : +·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· +*/ + +package sifpath + +import ( + "path/filepath" + "testing" +) + +func TestUserSubdirLayout(t *testing.T) { + got, err := UserSubdir("modules") + if err != nil { + t.Fatalf("UserSubdir returned error: %v", err) + } + if !filepath.IsAbs(got) { + t.Errorf("UserSubdir(%q) = %q, want an absolute path", "modules", got) + } + if base := filepath.Base(got); base != "modules" { + t.Errorf("UserSubdir(%q) base = %q, want %q", "modules", base, "modules") + } + if parent := filepath.Base(filepath.Dir(got)); parent != "sif" { + t.Errorf("UserSubdir(%q) parent = %q, want %q", "modules", parent, "sif") + } +} + +func TestUserSubdirSiblings(t *testing.T) { + mods, err := UserSubdir("modules") + if err != nil { + t.Fatalf("UserSubdir(modules): %v", err) + } + sigs, err := UserSubdir("signatures") + if err != nil { + t.Fatalf("UserSubdir(signatures): %v", err) + } + if filepath.Dir(mods) != filepath.Dir(sigs) { + t.Errorf("modules dir %q and signatures dir %q should share a parent", mods, sigs) + } +}