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.
This commit is contained in:
Tigah
2026-07-22 12:35:36 -07:00
committed by GitHub
parent f8f3b8cca6
commit 2cfee4cc3f
3 changed files with 86 additions and 0 deletions
+12
View File
@@ -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 // Generate requests based on paths and payloads
requests, err := generateHTTPRequests(target, cfg) requests, err := generateHTTPRequests(target, cfg)
if err != nil { if err != nil {
+73
View File
@@ -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))
}
}
+1
View File
@@ -61,6 +61,7 @@ type HTTPConfig struct {
Body string `yaml:"body,omitempty"` Body string `yaml:"body,omitempty"`
Attack string `yaml:"attack,omitempty"` // clusterbomb (default), pitchfork Attack string `yaml:"attack,omitempty"` // clusterbomb (default), pitchfork
Threads int `yaml:"threads,omitempty"` Threads int `yaml:"threads,omitempty"`
DisableRedirects bool `yaml:"disable-redirects,omitempty"` // stop at the first response; don't follow 3xx
Matchers []Matcher `yaml:"matchers"` Matchers []Matcher `yaml:"matchers"`
MatchersCondition string `yaml:"matchers-condition,omitempty"` // and (default), or MatchersCondition string `yaml:"matchers-condition,omitempty"` // and (default), or
Extractors []Extractor `yaml:"extractors,omitempty"` Extractors []Extractor `yaml:"extractors,omitempty"`