Skip to content

Commit 231d88d

Browse files
h0tak88rclaude
andcommitted
fix(scanner): path-traversal hardening, NS-takeover detection, resource/race fixes
From an adversarial audit of internal/scanner/** (21 confirmed findings). Fixes all HIGH + MEDIUM issues. HIGH — path traversal (arbitrary file write + dir deletion): Every scanner built its output dir as filepath.Join(resultsDir, <target>) where the target (domain/subdomain/bucket/repo) was never stripped of '/' or '..'. Since filepath.Join cleans the path, a target like "../../../../tmp/x" escaped the results dir; the subdomain scan's end-of-run cleanup (RemoveAll) made it an arbitrary-directory-deletion primitive, reachable from the unauthenticated-by- default scan API. - New utils.SanitizeTargetSegment(): collapses a target to one safe path segment (no separators, no ".."). Applied in utils.ResultsDir and at every direct path-build site: subdomain, nuclei, gf, sqlmap, dalfox, githubscan, backup, domain, ports, s3, asr, dns, misconfig, cnames, jsendpoints, urls, zerodays, exposure, depconfusion. - Regression test asserts no sanitized segment can contain a separator/".." and cannot escape via filepath.Join. HIGH — NS-takeover detection was dead code (dns.go): runNSTakeover built its resolver with dnsx.DefaultOptions, which only queries A records, so result.NS was always empty and NS takeovers were never detected. Now configures QuestionTypes=[]uint16{dns.TypeNS} so the NS section is populated. MEDIUM: - jsscan: cap remote JS body with io.LimitReader(10MB) — unbounded io.ReadAll across ~100 workers could exhaust memory. - cnames: on timeout, snapshot cnameRecords under the mutex before reading (workers could still append → data race). - asr/dns_asr: puredns I/O used fixed /tmp filenames shared across runs; now uses a per-invocation os.MkdirTemp dir so concurrent ASR scans don't corrupt each other. go mod tidy promotes miekg/dns (now used directly) and gjson to direct deps. CI replicated locally: CGO_ENABLED=1 go vet/build/test all pass; new utils tests and backup tests pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 500edff commit 231d88d

23 files changed

Lines changed: 185 additions & 125 deletions

File tree

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ require (
2121
github.com/joeguo/tldextract v0.0.0-20220507100122-d83daa6adef8
2222
github.com/juju/persistent-cookiejar v1.0.0
2323
github.com/majd/ipatool/v2 v2.2.0
24+
github.com/miekg/dns v1.1.68
2425
github.com/projectdiscovery/dnsx v1.2.2
2526
github.com/projectdiscovery/goflags v0.1.74
2627
github.com/projectdiscovery/httpx v1.8.1
@@ -35,6 +36,7 @@ require (
3536
github.com/sirupsen/logrus v1.9.3
3637
github.com/spf13/cobra v1.10.2
3738
github.com/sw33tLie/bbscope v0.0.0-20251113222800-c453973e83dd
39+
github.com/tidwall/gjson v1.18.0
3840
go.opentelemetry.io/otel v1.38.0
3941
go.opentelemetry.io/otel/sdk v1.38.0
4042
go.opentelemetry.io/otel/trace v1.38.0
@@ -285,7 +287,6 @@ require (
285287
github.com/mholt/archives v0.1.5 // indirect
286288
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
287289
github.com/microsoft/go-mssqldb v1.9.2 // indirect
288-
github.com/miekg/dns v1.1.68 // indirect
289290
github.com/mikelolasagasti/xz v1.0.1 // indirect
290291
github.com/minio/minlz v1.0.1 // indirect
291292
github.com/minio/selfupdate v0.6.1-0.20230907112617-f11e74f84ca7 // indirect
@@ -388,7 +389,6 @@ require (
388389
github.com/syndtr/goleveldb v1.0.0 // indirect
389390
github.com/tidwall/btree v1.8.1 // indirect
390391
github.com/tidwall/buntdb v1.3.2 // indirect
391-
github.com/tidwall/gjson v1.18.0 // indirect
392392
github.com/tidwall/grect v0.1.4 // indirect
393393
github.com/tidwall/match v1.2.0 // indirect
394394
github.com/tidwall/pretty v1.2.1 // indirect

internal/scanner/asr/internal/dns_asr/dns.go

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -96,11 +96,14 @@ func (c *Client) BruteforceWithWordlistFile(ctx context.Context, domain, wordlis
9696
// ---- puredns implementations (fast, for large lists) ----
9797

9898
func (c *Client) resolveWithPuredns(ctx context.Context, domains []string, threads int) ([]string, error) {
99-
tmpDir := os.TempDir()
100-
inputFile := filepath.Join(tmpDir, "asr_resolve_input.txt")
101-
outputFile := filepath.Join(tmpDir, "asr_resolve_output.txt")
102-
defer os.Remove(inputFile)
103-
defer os.Remove(outputFile)
99+
// Per-invocation temp dir so concurrent ASR scans don't clobber each other's I/O.
100+
tmpDir, err := os.MkdirTemp("", "asr-resolve-")
101+
if err != nil {
102+
return nil, fmt.Errorf("failed to create temp dir: %w", err)
103+
}
104+
defer os.RemoveAll(tmpDir)
105+
inputFile := filepath.Join(tmpDir, "input.txt")
106+
outputFile := filepath.Join(tmpDir, "output.txt")
104107

105108
// Write domains to temp file
106109
if err := writeLines(inputFile, domains); err != nil {
@@ -125,9 +128,12 @@ func (c *Client) resolveWithPuredns(ctx context.Context, domains []string, threa
125128
}
126129

127130
func (c *Client) bruteforceWithPuredns(ctx context.Context, domain string, wordlist []string, threads int) ([]string, error) {
128-
tmpDir := os.TempDir()
129-
wordlistFile := filepath.Join(tmpDir, "asr_bruteforce_wordlist.txt")
130-
defer os.Remove(wordlistFile)
131+
tmpDir, err := os.MkdirTemp("", "asr-bruteforce-")
132+
if err != nil {
133+
return nil, fmt.Errorf("failed to create temp dir: %w", err)
134+
}
135+
defer os.RemoveAll(tmpDir)
136+
wordlistFile := filepath.Join(tmpDir, "wordlist.txt")
131137

132138
if err := writeLines(wordlistFile, wordlist); err != nil {
133139
return nil, fmt.Errorf("failed to write wordlist file: %w", err)
@@ -137,9 +143,12 @@ func (c *Client) bruteforceWithPuredns(ctx context.Context, domain string, wordl
137143
}
138144

139145
func (c *Client) bruteforceFileWithPuredns(ctx context.Context, domain, wordlistPath string, threads int) ([]string, error) {
140-
tmpDir := os.TempDir()
141-
outputFile := filepath.Join(tmpDir, "asr_bruteforce_output.txt")
142-
defer os.Remove(outputFile)
146+
tmpDir, err := os.MkdirTemp("", "asr-bruteforce-out-")
147+
if err != nil {
148+
return nil, fmt.Errorf("failed to create temp dir: %w", err)
149+
}
150+
defer os.RemoveAll(tmpDir)
151+
outputFile := filepath.Join(tmpDir, "output.txt")
143152

144153
args := []string{"bruteforce", wordlistPath, domain, "-w", outputFile}
145154
if c.resolvers != "" {

internal/scanner/asr/modes.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ func RunMode1(ctx context.Context, opts Options) error {
2626
opts.Progress(fmt.Sprintf("Found %d passive subdomains", len(subs)))
2727

2828
resultsRoot := utils.GetResultsDir()
29-
domainDir := filepath.Join(resultsRoot, opts.Domain)
29+
domainDir := filepath.Join(resultsRoot, utils.SanitizeTargetSegment(opts.Domain))
3030
os.MkdirAll(domainDir, 0755)
3131

3232
return utils.WriteLines(filepath.Join(domainDir, "all_subs_passive.txt"), subs)
@@ -37,7 +37,7 @@ func RunMode2(ctx context.Context, opts Options) error {
3737
opts.Progress("Running Mode 2: DNS Bruteforce + TLS Probing + Permutations")
3838

3939
resultsRoot := utils.GetResultsDir()
40-
domainDir := filepath.Join(resultsRoot, opts.Domain)
40+
domainDir := filepath.Join(resultsRoot, utils.SanitizeTargetSegment(opts.Domain))
4141
os.MkdirAll(domainDir, 0755)
4242

4343
var allSubdomains []string
@@ -76,7 +76,7 @@ func RunMode3(ctx context.Context, opts Options) error {
7676
opts.Progress("Running Mode 3: Passive + TLS + DNS Bruteforce + HTTP Check + Scraping")
7777

7878
resultsRoot := utils.GetResultsDir()
79-
domainDir := filepath.Join(resultsRoot, opts.Domain)
79+
domainDir := filepath.Join(resultsRoot, utils.SanitizeTargetSegment(opts.Domain))
8080
os.MkdirAll(domainDir, 0755)
8181

8282
var allSubdomains []string
@@ -137,7 +137,7 @@ func RunMode4(ctx context.Context, opts Options) error {
137137
opts.Progress("Running Mode 4: Passive + TLS + HTTP Check + Scraping (No DNS Bruteforce)")
138138

139139
resultsRoot := utils.GetResultsDir()
140-
domainDir := filepath.Join(resultsRoot, opts.Domain)
140+
domainDir := filepath.Join(resultsRoot, utils.SanitizeTargetSegment(opts.Domain))
141141
os.MkdirAll(domainDir, 0755)
142142

143143
var allSubdomains []string
@@ -185,7 +185,7 @@ func RunMode5(ctx context.Context, opts Options) error {
185185
opts.Progress("Running Mode 5: Full Recon")
186186

187187
resultsRoot := utils.GetResultsDir()
188-
domainDir := filepath.Join(resultsRoot, opts.Domain)
188+
domainDir := filepath.Join(resultsRoot, utils.SanitizeTargetSegment(opts.Domain))
189189
os.MkdirAll(domainDir, 0755)
190190

191191
var allSubdomains []string

internal/scanner/backup/backup.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,9 @@ func Run(opts Options) (*Result, error) {
7777
outDir := opts.OutputDir
7878
if outDir == "" {
7979
if opts.Domain != "" {
80-
// Sanitize domain for filesystem use (remove protocol, replace : with -)
81-
sanitizedDomain := sanitizeDomainForPath(opts.Domain)
80+
// Sanitize domain into a single safe path segment (prevents traversal
81+
// via a crafted target such as "../../etc").
82+
sanitizedDomain := utils.SanitizeTargetSegment(opts.Domain)
8283
outDir = filepath.Join(resultsDir, sanitizedDomain, "backup")
8384
} else {
8485
outDir = filepath.Join(resultsDir, "backup")

internal/scanner/cnames/cnames.go

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ import (
1111
"sync"
1212
"time"
1313

14-
"github.com/h0tak88r/AutoAR/internal/scanner/subdomains"
1514
"github.com/h0tak88r/AutoAR/internal/db"
15+
"github.com/h0tak88r/AutoAR/internal/scanner/subdomains"
1616
"github.com/h0tak88r/AutoAR/internal/utils"
1717
"github.com/projectdiscovery/dnsx/libs/dnsx"
1818
)
@@ -38,7 +38,7 @@ type Options struct {
3838
func CollectCNAMEs(domain string) (*Result, error) {
3939
return CollectCNAMEsWithOptions(Options{
4040
Domain: domain,
41-
Threads: 100, // Default 100 concurrent DNS queries
41+
Threads: 100, // Default 100 concurrent DNS queries
4242
Timeout: 5 * time.Minute, // 5 minute timeout
4343
})
4444
}
@@ -48,7 +48,7 @@ func CollectCNAMEsWithOptions(opts Options) (*Result, error) {
4848
// Determine which mode we're in and set targets accordingly
4949
var targets []string
5050
var domain string
51-
51+
5252
if len(opts.Targets) > 0 {
5353
// Mode 1: Direct targets provided
5454
targets = opts.Targets
@@ -77,7 +77,7 @@ func CollectCNAMEsWithOptions(opts Options) (*Result, error) {
7777
// Mode 3: Domain provided - enumerate subdomains (original behavior)
7878
domain = opts.Domain
7979
resultsDir := utils.GetResultsDir()
80-
domainDir := filepath.Join(resultsDir, domain)
80+
domainDir := filepath.Join(resultsDir, utils.SanitizeTargetSegment(domain))
8181
subsDir := filepath.Join(domainDir, "subs")
8282
if err := utils.EnsureDir(subsDir); err != nil {
8383
return nil, fmt.Errorf("failed to create subs dir: %w", err)
@@ -140,7 +140,7 @@ func CollectCNAMEsWithOptions(opts Options) (*Result, error) {
140140

141141
// Set up output directory
142142
resultsDir := utils.GetResultsDir()
143-
domainDir := filepath.Join(resultsDir, domain)
143+
domainDir := filepath.Join(resultsDir, utils.SanitizeTargetSegment(domain))
144144
subsDir := filepath.Join(domainDir, "subs")
145145
if err := utils.EnsureDir(subsDir); err != nil {
146146
return nil, fmt.Errorf("failed to create subs dir: %w", err)
@@ -205,7 +205,7 @@ func CollectCNAMEsWithOptions(opts Options) (*Result, error) {
205205
cnameRecords = append(cnameRecords, fmt.Sprintf("%s CNAME %s", target, cname))
206206
}
207207
recordsMutex.Unlock()
208-
208+
209209
// Sync to core database Subdomains table
210210
go db.UpdateSubdomainCNAME(opts.Domain, target, strings.Join(results.CNAME, ","))
211211
}
@@ -239,8 +239,15 @@ func CollectCNAMEsWithOptions(opts Options) (*Result, error) {
239239
logger.GetLogger().Infof("[WARN] CNAME collection timed out after %v", opts.Timeout)
240240
}
241241

242+
// On timeout some workers may still be appending to cnameRecords, so take a
243+
// snapshot under the lock and use it for all reads below (avoids a data race).
244+
recordsMutex.Lock()
245+
recordsSnapshot := make([]string, len(cnameRecords))
246+
copy(recordsSnapshot, cnameRecords)
247+
recordsMutex.Unlock()
248+
242249
// Write results to file
243-
if err := writeLines(out, cnameRecords); err != nil {
250+
if err := writeLines(out, recordsSnapshot); err != nil {
244251
return nil, fmt.Errorf("failed to write CNAME records: %w", err)
245252
}
246253

@@ -260,8 +267,8 @@ func CollectCNAMEsWithOptions(opts Options) (*Result, error) {
260267
Finding string `json:"finding"`
261268
}
262269
var entries []cnameEntry
263-
seen := make(map[string]struct{}, len(cnameRecords))
264-
for _, rec := range cnameRecords {
270+
seen := make(map[string]struct{}, len(recordsSnapshot))
271+
for _, rec := range recordsSnapshot {
265272
// format: "sub.example.com CNAME target.example.com"
266273
parts := strings.Fields(rec)
267274
if len(parts) >= 3 {

internal/scanner/dalfox/dalfox.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ func RunDalfox(domain string, threads int) (*Result, error) {
3333
}
3434

3535
resultsDir := utils.GetResultsDir()
36-
domainDir := filepath.Join(resultsDir, domain)
36+
domainDir := filepath.Join(resultsDir, utils.SanitizeTargetSegment(domain))
3737
urlsFile := filepath.Join(domainDir, "urls", "all-urls.txt")
3838
inFile := filepath.Join(domainDir, "vulnerabilities", "xss", gf.ResultFileForPattern("xss"))
3939
outFile := filepath.Join(domainDir, "dalfox-results.txt")

internal/scanner/depconfusion/depconfusion.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -124,9 +124,9 @@ func runWebFromFile(opts Options, resultsDir string) error {
124124
outputDir := opts.OutputDir
125125
if outputDir == "" {
126126
if opts.Domain != "" {
127-
outputDir = filepath.Join(resultsDir, opts.Domain, "depconfusion", "web-file")
127+
outputDir = filepath.Join(resultsDir, utils.SanitizeTargetSegment(opts.Domain), "depconfusion", "web-file")
128128
} else if opts.Subdomain != "" {
129-
outputDir = filepath.Join(resultsDir, opts.Subdomain, "depconfusion", "web-file")
129+
outputDir = filepath.Join(resultsDir, utils.SanitizeTargetSegment(opts.Subdomain), "depconfusion", "web-file")
130130
} else {
131131
outputDir = filepath.Join(resultsDir, "depconfusion", "web-file")
132132
}
@@ -172,7 +172,7 @@ func runWebFull(opts Options, resultsDir string) error {
172172
return fmt.Errorf("domain is required for full scan")
173173
}
174174

175-
outputDir := filepath.Join(resultsDir, "depconfusion", fmt.Sprintf("web-full-%s", domain))
175+
outputDir := filepath.Join(resultsDir, "depconfusion", fmt.Sprintf("web-full-%s", utils.SanitizeTargetSegment(domain)))
176176
if opts.OutputDir != "" {
177177
outputDir = opts.OutputDir
178178
}
@@ -350,7 +350,7 @@ func runGitHubRepo(opts Options, resultsDir string) error {
350350
}
351351

352352
func runGitHubOrg(opts Options, resultsDir string) error {
353-
outputDir := filepath.Join(resultsDir, "depconfusion", fmt.Sprintf("github-org-%s", opts.GitHubOrg))
353+
outputDir := filepath.Join(resultsDir, "depconfusion", fmt.Sprintf("github-org-%s", utils.SanitizeTargetSegment(opts.GitHubOrg)))
354354
if opts.OutputDir != "" {
355355
outputDir = opts.OutputDir
356356
}

0 commit comments

Comments
 (0)