Skip to content

Commit 989c0d5

Browse files
feat: enable auth testing in quick mode and parallelize SCIM scanning
1 parent b87b4ad commit 989c0d5

8 files changed

Lines changed: 717 additions & 57 deletions

File tree

cmd/orchestrator_main.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,20 +103,20 @@ func buildOrchestratorConfig(cmd *cobra.Command) orchestrator.BugBountyConfig {
103103

104104
// Apply mode-specific settings
105105
if quick {
106-
// Quick mode: Fast triage, critical vulns only (< 10 seconds total)
106+
// Quick mode: Fast triage, critical vulns only (< 30 seconds total)
107107
// SKIP discovery entirely - just test the target directly
108108
config.SkipDiscovery = true
109109
config.DiscoveryTimeout = 1 * time.Second // Not used when skipped
110110
config.ScanTimeout = 30 * time.Second
111111
config.TotalTimeout = 1 * time.Minute
112112
config.MaxAssets = 1
113-
config.MaxDepth = 0
113+
config.MaxDepth = 1 // Allow minimal depth for auth endpoint discovery
114114
config.EnableDNS = false
115115
config.EnablePortScan = false
116116
config.EnableWebCrawl = false
117117
config.EnableAPITesting = false // Skip in quick mode
118118
config.EnableLogicTesting = false
119-
config.EnableAuthTesting = false // Skip auth discovery in quick mode
119+
config.EnableAuthTesting = true // ENABLED: Auth testing is high-value for bug bounties
120120
} else if deep {
121121
// Deep mode: Comprehensive testing (< 15 minutes total)
122122
config.DiscoveryTimeout = 1 * time.Minute

cmd/root_bounty_workflow_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -303,14 +303,14 @@ func TestRealWorldQuickScan(t *testing.T) {
303303
}
304304
defer store.Close()
305305

306-
// Quick mode config (should skip discovery)
306+
// Quick mode config (should skip discovery but enable auth testing)
307307
engineConfig := orchestrator.DefaultBugBountyConfig()
308308
engineConfig.SkipDiscovery = true // Quick mode skips discovery
309309
engineConfig.DiscoveryTimeout = 1 * time.Second
310-
engineConfig.TotalTimeout = 10 * time.Second
310+
engineConfig.TotalTimeout = 30 * time.Second // Increased for auth testing
311311
engineConfig.MaxAssets = 1
312312
engineConfig.ShowProgress = false
313-
engineConfig.EnableAuthTesting = false // Skip auth discovery in quick mode
313+
engineConfig.EnableAuthTesting = true // Enable auth testing - high value for bug bounties
314314

315315
engine, _ := orchestrator.NewBugBountyEngine(store, &noopTelemetry{}, log, engineConfig)
316316

internal/httpclient/factory.go

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
// Package httpclient provides secure HTTP clients with built-in protections
2+
package httpclient
3+
4+
import (
5+
"context"
6+
"fmt"
7+
"net"
8+
"net/http"
9+
"time"
10+
)
11+
12+
// SecureClientConfig configures the secure HTTP client
13+
type SecureClientConfig struct {
14+
Timeout time.Duration
15+
EnableSSRF bool // If true, blocks requests to private IPs
16+
FollowRedirects bool
17+
MaxRedirects int
18+
}
19+
20+
// DefaultConfig returns a secure default configuration
21+
func DefaultConfig() SecureClientConfig {
22+
return SecureClientConfig{
23+
Timeout: 30 * time.Second,
24+
EnableSSRF: true, // SSRF protection enabled by default
25+
FollowRedirects: true,
26+
MaxRedirects: 10,
27+
}
28+
}
29+
30+
// NewSecureClient creates an HTTP client with security protections
31+
// - Timeout enforcement (prevents hung requests)
32+
// - SSRF protection (blocks private IPs if enabled)
33+
// - Context-aware (respects context cancellation)
34+
// - Configurable redirect following
35+
func NewSecureClient(config SecureClientConfig) *http.Client {
36+
transport := &http.Transport{
37+
// Enable context-aware dialing
38+
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
39+
// SSRF Protection: Block private IPs if enabled
40+
if config.EnableSSRF {
41+
if err := validateAddress(addr); err != nil {
42+
return nil, fmt.Errorf("SSRF protection: %w", err)
43+
}
44+
}
45+
46+
// Use context-aware dialer
47+
var dialer net.Dialer
48+
return dialer.DialContext(ctx, network, addr)
49+
},
50+
51+
// Connection pool settings
52+
MaxIdleConns: 100,
53+
MaxIdleConnsPerHost: 10,
54+
IdleConnTimeout: 90 * time.Second,
55+
56+
// Timeouts
57+
TLSHandshakeTimeout: 10 * time.Second,
58+
ResponseHeaderTimeout: 10 * time.Second,
59+
ExpectContinueTimeout: 1 * time.Second,
60+
}
61+
62+
client := &http.Client{
63+
Timeout: config.Timeout,
64+
Transport: transport,
65+
}
66+
67+
// Configure redirect policy
68+
if !config.FollowRedirects {
69+
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
70+
return http.ErrUseLastResponse
71+
}
72+
} else if config.MaxRedirects > 0 {
73+
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
74+
if len(via) >= config.MaxRedirects {
75+
return fmt.Errorf("stopped after %d redirects", config.MaxRedirects)
76+
}
77+
78+
// SSRF protection on redirects
79+
if config.EnableSSRF {
80+
if err := validateURL(req.URL.String()); err != nil {
81+
return fmt.Errorf("SSRF protection on redirect: %w", err)
82+
}
83+
}
84+
85+
return nil
86+
}
87+
}
88+
89+
return client
90+
}
91+
92+
// NewQuickClient creates a client optimized for quick scans
93+
// - Short timeout (10 seconds)
94+
// - SSRF protection enabled
95+
// - No redirect following
96+
func NewQuickClient() *http.Client {
97+
return NewSecureClient(SecureClientConfig{
98+
Timeout: 10 * time.Second,
99+
EnableSSRF: true,
100+
FollowRedirects: false,
101+
MaxRedirects: 0,
102+
})
103+
}
104+
105+
// NewScannerClient creates a client optimized for security scanning
106+
// - Medium timeout (30 seconds)
107+
// - SSRF protection enabled
108+
// - Limited redirects
109+
func NewScannerClient() *http.Client {
110+
return NewSecureClient(SecureClientConfig{
111+
Timeout: 30 * time.Second,
112+
EnableSSRF: true,
113+
FollowRedirects: true,
114+
MaxRedirects: 5,
115+
})
116+
}
117+
118+
// NewDiscoveryClient creates a client optimized for asset discovery
119+
// - Longer timeout (60 seconds)
120+
// - SSRF protection enabled
121+
// - Follow redirects
122+
func NewDiscoveryClient() *http.Client {
123+
return NewSecureClient(SecureClientConfig{
124+
Timeout: 60 * time.Second,
125+
EnableSSRF: true,
126+
FollowRedirects: true,
127+
MaxRedirects: 10,
128+
})
129+
}
130+
131+
// NewUnsafeClient creates a client WITHOUT SSRF protection
132+
// Use only when scanning authorized internal networks
133+
func NewUnsafeClient(timeout time.Duration) *http.Client {
134+
return NewSecureClient(SecureClientConfig{
135+
Timeout: timeout,
136+
EnableSSRF: false, // SSRF protection DISABLED
137+
FollowRedirects: true,
138+
MaxRedirects: 10,
139+
})
140+
}
141+
142+
// validateAddress checks if an address points to a private IP
143+
func validateAddress(addr string) error {
144+
// Split host and port
145+
host, _, err := net.SplitHostPort(addr)
146+
if err != nil {
147+
// Try without port
148+
host = addr
149+
}
150+
151+
// Resolve to IP addresses
152+
ips, err := net.LookupIP(host)
153+
if err != nil {
154+
return fmt.Errorf("failed to resolve %s: %w", host, err)
155+
}
156+
157+
// Check each resolved IP
158+
for _, ip := range ips {
159+
if isPrivateIP(ip) {
160+
return fmt.Errorf("blocked private IP: %s (%s)", ip, host)
161+
}
162+
}
163+
164+
return nil
165+
}
166+
167+
// validateURL checks if a URL is safe (not pointing to private IPs)
168+
func validateURL(urlStr string) error {
169+
// For URL validation, extract host and validate
170+
// This is a simplified version - full implementation would parse URL properly
171+
return nil // Placeholder
172+
}
173+
174+
// isPrivateIP checks if an IP address is private, loopback, or link-local
175+
func isPrivateIP(ip net.IP) bool {
176+
// Check for loopback
177+
if ip.IsLoopback() {
178+
return true
179+
}
180+
181+
// Check for link-local
182+
if ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() {
183+
return true
184+
}
185+
186+
// Check for private IP ranges
187+
if ip.IsPrivate() {
188+
return true
189+
}
190+
191+
// Check for special addresses
192+
if ip.String() == "0.0.0.0" || ip.String() == "::" {
193+
return true
194+
}
195+
196+
// Check for IPv4 private ranges manually (as backup)
197+
if ip4 := ip.To4(); ip4 != nil {
198+
// 10.0.0.0/8
199+
if ip4[0] == 10 {
200+
return true
201+
}
202+
// 172.16.0.0/12
203+
if ip4[0] == 172 && ip4[1] >= 16 && ip4[1] <= 31 {
204+
return true
205+
}
206+
// 192.168.0.0/16
207+
if ip4[0] == 192 && ip4[1] == 168 {
208+
return true
209+
}
210+
// 127.0.0.0/8 (loopback)
211+
if ip4[0] == 127 {
212+
return true
213+
}
214+
// 169.254.0.0/16 (link-local)
215+
if ip4[0] == 169 && ip4[1] == 254 {
216+
return true
217+
}
218+
}
219+
220+
return false
221+
}
222+
223+
// DoWithContext performs an HTTP request with context enforcement
224+
// This ensures the request respects context cancellation and deadlines
225+
func DoWithContext(ctx context.Context, client *http.Client, req *http.Request) (*http.Response, error) {
226+
// Clone request with context
227+
req = req.WithContext(ctx)
228+
229+
// Perform request
230+
resp, err := client.Do(req)
231+
if err != nil {
232+
// Check if error was due to context cancellation
233+
if ctx.Err() != nil {
234+
return nil, fmt.Errorf("request cancelled: %w", ctx.Err())
235+
}
236+
return nil, err
237+
}
238+
239+
return resp, nil
240+
}

0 commit comments

Comments
 (0)