Skip to content

Commit 7b2f881

Browse files
h0tak88rclaude
andcommitted
fix(scanner): resolve LOW audit findings (resource leaks, timeouts, races)
From the scanner audit (LOW severity batch). - ffuf try403Bypass (ffuf.go): closed response bodies per iteration instead of `defer` inside the bypass-technique loop (bodies/connections no longer accumulate until the function returns). - bufio.Scanner 64KB limit (tempfiles.go, jsscan.go, urls.go, reflection.go, jsendpoints.go): raise the per-line buffer to 16MB so long Wayback/CDX URL lines no longer abort the read with ErrTooLong. - cnames (cnames.go): call db.UpdateSubdomainCNAME synchronously inside the worker instead of a fire-and-forget `go` (was an unbounded goroutine leak that could outlive the function); use the derived root `domain`, not opts.Domain, so Targets/Subdomain modes record the correct key. - githubscan (githubscan.go): run TruffleHog via exec.CommandContext with a 30-minute timeout so a hung/huge org/repo can't pin the scan forever. - subdomainmonitor (daemon.go): add an in-flight guard so a scan that outlasts its interval isn't started again by the next ticker pass (no more duplicate DB writes / duplicate webhook alerts). - AEM (aem/scanner.go): close response bodies on every path. checkWebDAV never closed; ~20 probe loops closed only on HTTP 200, leaking every non-200 body. Added a per-request deferred close after each error check. CI replicated locally: CGO_ENABLED=1 go vet/build/test all pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent e0fdee4 commit 7b2f881

10 files changed

Lines changed: 133 additions & 66 deletions

File tree

internal/scanner/aem/scanner.go

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ type ScannerCheck func(baseURL string, ssrfHost string, client *HTTPClient) []Fi
1515

1616
var (
1717
scannerChecks = make(map[string]ScannerCheck)
18-
scannerMutex sync.Mutex
18+
scannerMutex sync.Mutex
1919
)
2020

2121
// RegisterScannerCheck registers a vulnerability check
@@ -152,6 +152,7 @@ func checkGetServlet(baseURL string, ssrfHost string, client *HTTPClient) []Find
152152
if err != nil {
153153
continue
154154
}
155+
defer resp.Body.Close()
155156

156157
if resp.StatusCode == 200 {
157158
body, _ := io.ReadAll(resp.Body)
@@ -209,6 +210,7 @@ func checkQueryBuilderServlet(baseURL string, ssrfHost string, client *HTTPClien
209210
if err != nil {
210211
continue
211212
}
213+
defer resp.Body.Close()
212214

213215
if resp.StatusCode == 200 {
214216
body, _ := io.ReadAll(resp.Body)
@@ -269,6 +271,7 @@ func checkGQLServlet(baseURL string, ssrfHost string, client *HTTPClient) []Find
269271
if err != nil {
270272
continue
271273
}
274+
defer resp.Body.Close()
272275

273276
if resp.StatusCode == 200 {
274277
body, _ := io.ReadAll(resp.Body)
@@ -316,6 +319,7 @@ func checkPostServlet(baseURL string, ssrfHost string, client *HTTPClient) []Fin
316319
if err != nil {
317320
continue
318321
}
322+
defer resp.Body.Close()
319323

320324
if resp.StatusCode == 200 {
321325
body, _ := io.ReadAll(resp.Body)
@@ -361,6 +365,7 @@ func checkCreateNewNodes(baseURL string, ssrfHost string, client *HTTPClient) []
361365
if err != nil {
362366
continue
363367
}
368+
defer resp.Body.Close()
364369

365370
if resp.StatusCode == 200 || resp.StatusCode == 201 {
366371
body, _ := io.ReadAll(resp.Body)
@@ -401,6 +406,7 @@ func checkCreateNewNodes(baseURL string, ssrfHost string, client *HTTPClient) []
401406
if err != nil {
402407
continue
403408
}
409+
defer resp.Body.Close()
404410

405411
if resp.StatusCode == 200 || resp.StatusCode == 201 {
406412
body, _ := io.ReadAll(resp.Body)
@@ -444,6 +450,7 @@ func checkLoginStatusServlet(baseURL string, ssrfHost string, client *HTTPClient
444450
if err != nil {
445451
continue
446452
}
453+
defer resp.Body.Close()
447454

448455
if resp.StatusCode == 200 {
449456
body, _ := io.ReadAll(resp.Body)
@@ -466,6 +473,7 @@ func checkLoginStatusServlet(baseURL string, ssrfHost string, client *HTTPClient
466473
if err != nil {
467474
continue
468475
}
476+
defer resp2.Body.Close()
469477

470478
if resp2.StatusCode == 200 {
471479
body2, _ := io.ReadAll(resp2.Body)
@@ -512,6 +520,7 @@ func checkUserInfoServlet(baseURL string, ssrfHost string, client *HTTPClient) [
512520
if err != nil {
513521
continue
514522
}
523+
defer resp.Body.Close()
515524

516525
if resp.StatusCode == 200 {
517526
body, _ := io.ReadAll(resp.Body)
@@ -534,6 +543,7 @@ func checkUserInfoServlet(baseURL string, ssrfHost string, client *HTTPClient) [
534543
if err != nil {
535544
continue
536545
}
546+
defer resp2.Body.Close()
537547

538548
if resp2.StatusCode == 200 {
539549
body2, _ := io.ReadAll(resp2.Body)
@@ -582,6 +592,7 @@ func checkFelixConsole(baseURL string, ssrfHost string, client *HTTPClient) []Fi
582592
if err != nil {
583593
continue
584594
}
595+
defer resp.Body.Close()
585596

586597
if resp.StatusCode == 200 {
587598
body, _ := io.ReadAll(resp.Body)
@@ -621,6 +632,7 @@ func checkWCMDebugFilter(baseURL string, ssrfHost string, client *HTTPClient) []
621632
if err != nil {
622633
continue
623634
}
635+
defer resp.Body.Close()
624636

625637
if resp.StatusCode == 200 {
626638
body, _ := io.ReadAll(resp.Body)
@@ -663,6 +675,7 @@ func checkWCMSuggestionsServlet(baseURL string, ssrfHost string, client *HTTPCli
663675
if err != nil {
664676
continue
665677
}
678+
defer resp.Body.Close()
666679

667680
if resp.StatusCode == 200 {
668681
body, _ := io.ReadAll(resp.Body)
@@ -711,6 +724,7 @@ func checkCRXDECRX(baseURL string, ssrfHost string, client *HTTPClient) []Findin
711724
if err != nil {
712725
continue
713726
}
727+
defer resp.Body.Close()
714728

715729
if resp.StatusCode == 200 {
716730
body, _ := io.ReadAll(resp.Body)
@@ -765,6 +779,7 @@ func checkGroovyConsole(baseURL string, ssrfHost string, client *HTTPClient) []F
765779
if err != nil {
766780
continue
767781
}
782+
defer resp.Body.Close()
768783

769784
if resp.StatusCode == 200 {
770785
body, _ := io.ReadAll(resp.Body)
@@ -811,6 +826,7 @@ func checkGroovyConsole(baseURL string, ssrfHost string, client *HTTPClient) []F
811826
if err != nil {
812827
continue
813828
}
829+
defer resp.Body.Close()
814830

815831
if resp.StatusCode == 200 {
816832
body, _ := io.ReadAll(resp.Body)
@@ -862,6 +878,7 @@ func checkACSTools(baseURL string, ssrfHost string, client *HTTPClient) []Findin
862878
if err != nil {
863879
continue
864880
}
881+
defer resp.Body.Close()
865882

866883
if resp.StatusCode == 200 {
867884
body, _ := io.ReadAll(resp.Body)
@@ -882,16 +899,17 @@ func checkACSTools(baseURL string, ssrfHost string, client *HTTPClient) []Findin
882899
// Check predicates endpoint
883900
predicatesURL := NormalizeURL(baseURL, "/bin/acs-tools/qe/predicates.json")
884901
resp, err := client.Get(predicatesURL, nil)
885-
if err == nil && resp.StatusCode == 200 {
886-
body, _ := io.ReadAll(resp.Body)
887-
resp.Body.Close()
888-
889-
if strings.Contains(string(body), "relativedaterange") {
890-
findings = append(findings, Finding{
891-
Name: "ACSTools",
892-
URL: predicatesURL,
893-
Description: "ACS Tools predicates.",
894-
})
902+
if err == nil {
903+
defer resp.Body.Close()
904+
if resp.StatusCode == 200 {
905+
body, _ := io.ReadAll(resp.Body)
906+
if strings.Contains(string(body), "relativedaterange") {
907+
findings = append(findings, Finding{
908+
Name: "ACSTools",
909+
URL: predicatesURL,
910+
Description: "ACS Tools predicates.",
911+
})
912+
}
895913
}
896914
}
897915

@@ -917,6 +935,7 @@ func checkWebDAV(baseURL string, ssrfHost string, client *HTTPClient) []Finding
917935
if err != nil {
918936
continue
919937
}
938+
defer resp.Body.Close()
920939

921940
if resp.StatusCode == 401 {
922941
wwwAuth := strings.ToLower(resp.Header.Get("WWW-Authenticate"))
@@ -956,6 +975,7 @@ func checkSetPreferences(baseURL string, ssrfHost string, client *HTTPClient) []
956975
if err != nil {
957976
continue
958977
}
978+
defer resp.Body.Close()
959979

960980
if resp.StatusCode == 400 {
961981
body, _ := io.ReadAll(resp.Body)
@@ -997,6 +1017,7 @@ func checkMergeMetadata(baseURL string, ssrfHost string, client *HTTPClient) []F
9971017
if err != nil {
9981018
continue
9991019
}
1020+
defer resp.Body.Close()
10001021

10011022
if resp.StatusCode == 200 {
10021023
body, _ := io.ReadAll(resp.Body)
@@ -1052,6 +1073,7 @@ func checkGuideInternalSubmitServlet(baseURL string, ssrfHost string, client *HT
10521073
if err != nil {
10531074
continue
10541075
}
1076+
defer resp.Body.Close()
10551077

10561078
if resp.StatusCode == 200 {
10571079
body, _ := io.ReadAll(resp.Body)
@@ -1110,6 +1132,7 @@ func checkReportingServicesServlet(baseURL string, ssrfHost string, client *HTTP
11101132
if err != nil {
11111133
continue
11121134
}
1135+
defer resp.Body.Close()
11131136
if resp.StatusCode == 200 {
11141137
resp.Body.Close()
11151138
return []Finding{{
@@ -1227,6 +1250,7 @@ func checkSWFXSS(baseURL string, ssrfHost string, client *HTTPClient) []Finding
12271250
if err != nil {
12281251
continue
12291252
}
1253+
defer resp.Body.Close()
12301254

12311255
if resp.StatusCode == 200 {
12321256
ct := ContentType(resp.Header.Get("Content-Type"))
@@ -1278,4 +1302,3 @@ func checkExternalJobServlet(baseURL string, ssrfHost string, client *HTTPClient
12781302
}
12791303
return nil
12801304
}
1281-

internal/scanner/cnames/cnames.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -206,8 +206,12 @@ func CollectCNAMEsWithOptions(opts Options) (*Result, error) {
206206
}
207207
recordsMutex.Unlock()
208208

209-
// Sync to core database Subdomains table
210-
go db.UpdateSubdomainCNAME(opts.Domain, target, strings.Join(results.CNAME, ","))
209+
// Sync to core database Subdomains table. Called synchronously
210+
// inside the worker goroutine (was a fire-and-forget `go` that
211+
// leaked unbounded goroutines and could outlive the function).
212+
// Use the derived root `domain`, not opts.Domain, so the key is
213+
// correct in Targets/Subdomain modes too.
214+
_ = db.UpdateSubdomainCNAME(domain, target, strings.Join(results.CNAME, ","))
211215
}
212216
}
213217
}()

internal/scanner/ffuf/ffuf.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import (
55
"context"
66
"crypto/tls"
77
"fmt"
8-
"io"
98
"github.com/h0tak88r/AutoAR/internal/logger"
9+
"io"
1010
"net/http"
1111
"os"
1212
"path/filepath"
@@ -705,12 +705,12 @@ func (c *customOutputProvider) try403Bypass(originalResp ffufpkg.Response) {
705705
if err != nil {
706706
continue
707707
}
708-
defer resp.Body.Close()
709708

710709
// Only process if bypass was successful (status 200)
711710
if resp.StatusCode == 200 {
712711
// Read response body to get content metrics
713712
bodyBytes, err := io.ReadAll(io.LimitReader(resp.Body, 1024*1024)) // Limit to 1MB
713+
resp.Body.Close()
714714
if err != nil {
715715
continue
716716
}
@@ -777,6 +777,9 @@ func (c *customOutputProvider) try403Bypass(originalResp ffufpkg.Response) {
777777
// Stop after first successful bypass to avoid duplicates
778778
return
779779
}
780+
// Non-200: close the body now so open connections don't accumulate
781+
// across the bypass-technique loop (defer would fire only on return).
782+
resp.Body.Close()
780783
}
781784
}
782785

internal/scanner/githubscan/githubscan.go

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
11
package githubscan
22

33
import (
4+
"context"
45
"encoding/json"
56
"fmt"
67
"os"
78
"os/exec"
89
"path/filepath"
910
"strings"
11+
"time"
1012

1113
"github.com/h0tak88r/AutoAR/internal/utils"
1214
)
1315

16+
// trufflehogTimeout bounds a single TruffleHog invocation so a hung or huge
17+
// org/repo can't pin the scan indefinitely.
18+
const trufflehogTimeout = 30 * time.Minute
19+
1420
type Mode string
1521

1622
const (
@@ -81,14 +87,19 @@ func Run(opts Options) (*Result, error) {
8187
jsonPath := filepath.Join(baseDir, "secrets.json")
8288
logPath := filepath.Join(baseDir, "trufflehog.log")
8389

84-
cmd, err := buildTrufflehogCommand(opts)
90+
ctx, cancel := context.WithTimeout(context.Background(), trufflehogTimeout)
91+
defer cancel()
92+
cmd, err := buildTrufflehogCommand(ctx, opts)
8593
if err != nil {
8694
return nil, err
8795
}
8896

8997
// Capture ALL output in one buffer. TruffleHog mixes JSON finding lines
9098
// and structured log lines (also JSON) on stdout; stderr is typically empty.
9199
rawOutput, runErr := cmd.CombinedOutput()
100+
if ctx.Err() == context.DeadlineExceeded {
101+
return nil, fmt.Errorf("trufflehog scan timed out after %s", trufflehogTimeout)
102+
}
92103

93104
// Split into JSON findings vs log/error lines.
94105
// A finding line starts with '{"SourceMetadata"' or just '{', while log
@@ -195,7 +206,7 @@ func Run(opts Options) (*Result, error) {
195206
return result, nil
196207
}
197208

198-
func buildTrufflehogCommand(opts Options) (*exec.Cmd, error) {
209+
func buildTrufflehogCommand(ctx context.Context, opts Options) (*exec.Cmd, error) {
199210
// Disable auto-update to avoid noise
200211
env := append(os.Environ(),
201212
"TRUFFLEHOG_NO_UPDATE=true",
@@ -206,7 +217,7 @@ func buildTrufflehogCommand(opts Options) (*exec.Cmd, error) {
206217
case ModeRepo:
207218
repoURL := ensureRepoURL(opts.Repo)
208219
args := []string{"git", repoURL, "--json", "--no-update"}
209-
cmd := exec.Command("trufflehog", args...)
220+
cmd := exec.CommandContext(ctx, "trufflehog", args...)
210221
cmd.Env = env
211222
return cmd, nil
212223

@@ -223,7 +234,7 @@ func buildTrufflehogCommand(opts Options) (*exec.Cmd, error) {
223234
"--no-update",
224235
"--token", os.Getenv("GITHUB_TOKEN"),
225236
}
226-
cmd := exec.Command("trufflehog", args...)
237+
cmd := exec.CommandContext(ctx, "trufflehog", args...)
227238
cmd.Env = env
228239
return cmd, nil
229240

@@ -242,7 +253,7 @@ func buildTrufflehogCommand(opts Options) (*exec.Cmd, error) {
242253
if token := os.Getenv("GITHUB_TOKEN"); token != "" {
243254
args = append(args, "--token", token)
244255
}
245-
cmd := exec.Command("trufflehog", args...)
256+
cmd := exec.CommandContext(ctx, "trufflehog", args...)
246257
cmd.Env = env
247258
return cmd, nil
248259
default:

internal/scanner/jsendpoints/jsendpoints.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ import (
1919

2020
// Options controls JS endpoint extraction behaviour.
2121
type Options struct {
22-
Domain string
23-
JSFile string // path to js-urls.txt; if empty, derived from Domain
24-
Threads int
22+
Domain string
23+
JSFile string // path to js-urls.txt; if empty, derived from Domain
24+
Threads int
2525
}
2626

2727
// Result summarises the extraction.
@@ -175,7 +175,7 @@ func extractEndpoints(content, sourceURL string) []string {
175175
continue
176176
}
177177
ep := strings.TrimSpace(m[1])
178-
ep = strings.Trim(ep, `"'` + "`")
178+
ep = strings.Trim(ep, `"'`+"`")
179179
if ep == "" || len(ep) < 3 || len(ep) > 300 {
180180
continue
181181
}
@@ -241,6 +241,7 @@ func readLines(path string) ([]string, error) {
241241
defer f.Close()
242242
var lines []string
243243
sc := bufio.NewScanner(f)
244+
sc.Buffer(make([]byte, 64*1024), 16*1024*1024)
244245
for sc.Scan() {
245246
if l := strings.TrimSpace(sc.Text()); l != "" {
246247
lines = append(lines, l)

0 commit comments

Comments
 (0)