Skip to content

Commit 2cfee4c

Browse files
authored
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.
1 parent f8f3b8c commit 2cfee4c

3 files changed

Lines changed: 86 additions & 0 deletions

File tree

internal/modules/executor.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,18 @@ func ExecuteHTTPModule(ctx context.Context, target string, def *YAMLModule, opts
7272
}
7373
}
7474

75+
// disable-redirects only applies to this module's requests; opts.Client may
76+
// be the shared httpx client reused by every other module in the run, so a
77+
// module-scoped policy shallow-copies it (keeping the pooled Transport)
78+
// rather than mutating CheckRedirect on the shared instance.
79+
if cfg.DisableRedirects {
80+
scoped := *client
81+
scoped.CheckRedirect = func(_ *http.Request, _ []*http.Request) error {
82+
return http.ErrUseLastResponse
83+
}
84+
client = &scoped
85+
}
86+
7587
// Generate requests based on paths and payloads
7688
requests, err := generateHTTPRequests(target, cfg)
7789
if err != nil {

internal/modules/redirect_test.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
3+
: :
4+
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
5+
: ▄█ █ █▀ · BSD 3-Clause License :
6+
: :
7+
: (c) 2022-2026 vmfunc, xyzeva, :
8+
: lunchcat alumni & contributors :
9+
: :
10+
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
11+
*/
12+
13+
package modules
14+
15+
import (
16+
"context"
17+
"net/http"
18+
"net/http/httptest"
19+
"testing"
20+
21+
"github.com/vmfunc/sif/internal/httpx"
22+
)
23+
24+
// a target whose root 302-redirects to a landing page: the redirect response
25+
// carries the signal (a Location header, the 302 status), the landing page does
26+
// not. matching that signal requires stopping at the 3xx.
27+
func redirectServer() *httptest.Server {
28+
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
29+
if r.URL.Path == "/landing" {
30+
w.WriteHeader(http.StatusOK)
31+
_, _ = w.Write([]byte("landed"))
32+
return
33+
}
34+
w.Header().Set("Location", "/landing")
35+
w.WriteHeader(http.StatusFound)
36+
}))
37+
}
38+
39+
func TestExecuteHTTPModuleDisableRedirects(t *testing.T) {
40+
srv := redirectServer()
41+
defer srv.Close()
42+
43+
mod := func(disable bool) *YAMLModule {
44+
return &YAMLModule{
45+
ID: "redirect",
46+
Type: TypeHTTP,
47+
HTTP: &HTTPConfig{
48+
Paths: []string{"{{BaseURL}}/"},
49+
DisableRedirects: disable,
50+
Matchers: []Matcher{
51+
{Type: "status", Status: []int{http.StatusFound}},
52+
},
53+
},
54+
}
55+
}
56+
opts := Options{Timeout: testTimeout, Client: httpx.Client(testTimeout)}
57+
58+
followed, err := ExecuteHTTPModule(context.Background(), srv.URL, mod(false), opts)
59+
if err != nil {
60+
t.Fatalf("ExecuteHTTPModule(follow): %v", err)
61+
}
62+
if len(followed.Findings) != 0 {
63+
t.Fatalf("with redirects followed, got %d findings matching 302, want 0", len(followed.Findings))
64+
}
65+
66+
stopped, err := ExecuteHTTPModule(context.Background(), srv.URL, mod(true), opts)
67+
if err != nil {
68+
t.Fatalf("ExecuteHTTPModule(no-follow): %v", err)
69+
}
70+
if len(stopped.Findings) != 1 {
71+
t.Fatalf("with redirects disabled, got %d findings matching 302, want 1", len(stopped.Findings))
72+
}
73+
}

internal/modules/yaml.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ type HTTPConfig struct {
6161
Body string `yaml:"body,omitempty"`
6262
Attack string `yaml:"attack,omitempty"` // clusterbomb (default), pitchfork
6363
Threads int `yaml:"threads,omitempty"`
64+
DisableRedirects bool `yaml:"disable-redirects,omitempty"` // stop at the first response; don't follow 3xx
6465
Matchers []Matcher `yaml:"matchers"`
6566
MatchersCondition string `yaml:"matchers-condition,omitempty"` // and (default), or
6667
Extractors []Extractor `yaml:"extractors,omitempty"`

0 commit comments

Comments
 (0)