From 2cfee4cc3f2d2987757b68c1efc8c6ea545b2417 Mon Sep 17 00:00:00 2001 From: Tigah <88289044+TBX3D@users.noreply.github.com> Date: Wed, 22 Jul 2026 12:35:36 -0700 Subject: [PATCH] feat(modules): add disable-redirects http option (#273) A module that fingerprints a redirect itself (open-redirect proofs, a Location header, a 3xx status) could not see the redirect: the executor followed 3xx to the final response before matchers ran, with no way to stop. Add an opt-in disable-redirects field that scopes an ErrUseLastResponse policy to the module's own client copy, so the shared httpx transport keeps following redirects for every other module. --- internal/modules/executor.go | 12 +++++ internal/modules/redirect_test.go | 73 +++++++++++++++++++++++++++++++ internal/modules/yaml.go | 1 + 3 files changed, 86 insertions(+) create mode 100644 internal/modules/redirect_test.go diff --git a/internal/modules/executor.go b/internal/modules/executor.go index f969581..3ac0971 100644 --- a/internal/modules/executor.go +++ b/internal/modules/executor.go @@ -72,6 +72,18 @@ func ExecuteHTTPModule(ctx context.Context, target string, def *YAMLModule, opts } } + // disable-redirects only applies to this module's requests; opts.Client may + // be the shared httpx client reused by every other module in the run, so a + // module-scoped policy shallow-copies it (keeping the pooled Transport) + // rather than mutating CheckRedirect on the shared instance. + if cfg.DisableRedirects { + scoped := *client + scoped.CheckRedirect = func(_ *http.Request, _ []*http.Request) error { + return http.ErrUseLastResponse + } + client = &scoped + } + // Generate requests based on paths and payloads requests, err := generateHTTPRequests(target, cfg) if err != nil { diff --git a/internal/modules/redirect_test.go b/internal/modules/redirect_test.go new file mode 100644 index 0000000..924afae --- /dev/null +++ b/internal/modules/redirect_test.go @@ -0,0 +1,73 @@ +/* +·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· +: : +: █▀ █ █▀▀ · Blazing-fast pentesting suite : +: ▄█ █ █▀ · BSD 3-Clause License : +: : +: (c) 2022-2026 vmfunc, xyzeva, : +: lunchcat alumni & contributors : +: : +·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· +*/ + +package modules + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/vmfunc/sif/internal/httpx" +) + +// a target whose root 302-redirects to a landing page: the redirect response +// carries the signal (a Location header, the 302 status), the landing page does +// not. matching that signal requires stopping at the 3xx. +func redirectServer() *httptest.Server { + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/landing" { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("landed")) + return + } + w.Header().Set("Location", "/landing") + w.WriteHeader(http.StatusFound) + })) +} + +func TestExecuteHTTPModuleDisableRedirects(t *testing.T) { + srv := redirectServer() + defer srv.Close() + + mod := func(disable bool) *YAMLModule { + return &YAMLModule{ + ID: "redirect", + Type: TypeHTTP, + HTTP: &HTTPConfig{ + Paths: []string{"{{BaseURL}}/"}, + DisableRedirects: disable, + Matchers: []Matcher{ + {Type: "status", Status: []int{http.StatusFound}}, + }, + }, + } + } + opts := Options{Timeout: testTimeout, Client: httpx.Client(testTimeout)} + + followed, err := ExecuteHTTPModule(context.Background(), srv.URL, mod(false), opts) + if err != nil { + t.Fatalf("ExecuteHTTPModule(follow): %v", err) + } + if len(followed.Findings) != 0 { + t.Fatalf("with redirects followed, got %d findings matching 302, want 0", len(followed.Findings)) + } + + stopped, err := ExecuteHTTPModule(context.Background(), srv.URL, mod(true), opts) + if err != nil { + t.Fatalf("ExecuteHTTPModule(no-follow): %v", err) + } + if len(stopped.Findings) != 1 { + t.Fatalf("with redirects disabled, got %d findings matching 302, want 1", len(stopped.Findings)) + } +} diff --git a/internal/modules/yaml.go b/internal/modules/yaml.go index d404239..ed13023 100644 --- a/internal/modules/yaml.go +++ b/internal/modules/yaml.go @@ -61,6 +61,7 @@ type HTTPConfig struct { Body string `yaml:"body,omitempty"` Attack string `yaml:"attack,omitempty"` // clusterbomb (default), pitchfork Threads int `yaml:"threads,omitempty"` + DisableRedirects bool `yaml:"disable-redirects,omitempty"` // stop at the first response; don't follow 3xx Matchers []Matcher `yaml:"matchers"` MatchersCondition string `yaml:"matchers-condition,omitempty"` // and (default), or Extractors []Extractor `yaml:"extractors,omitempty"`