Skip to content

feat: add automatic wildcard detection (--auto-wildcard)#962

Open
flaggdavid-source wants to merge 1 commit intoprojectdiscovery:devfrom
flaggdavid-source:feat/auto-wildcard-detection-924
Open

feat: add automatic wildcard detection (--auto-wildcard)#962
flaggdavid-source wants to merge 1 commit intoprojectdiscovery:devfrom
flaggdavid-source:feat/auto-wildcard-detection-924

Conversation

@flaggdavid-source
Copy link

@flaggdavid-source flaggdavid-source commented Mar 15, 2026

Summary

Fixes #924

Adds --auto-wildcard / -aw flag that automatically detects and filters wildcard DNS domains across all input, similar to how PureDNS handles wildcard detection.

How It Works

  1. Before resolution, extracts unique root domains from all inputs
  2. Probes each domain with a random subdomain (xid-generated, consistent with existing wildcard code)
  3. Compares response IPs — if a random subdomain returns the same IPs as the root domain, it's flagged as wildcard
  4. Post-processing filter removes results from detected wildcard domains

This eliminates the need to manually specify -wd for each domain, making wildcard filtering practical for large multi-domain scans.

Changes

File Change
internal/runner/options.go New AutoWildcard field, -aw flag, stream mode validation
internal/runner/wildcard.go AutoDetectWildcards(), detectWildcardForDomain(), getRootDomain(), IsAutoWildcardDomain() with thread-safe RWMutex
internal/runner/runner.go Integration: auto-detect before workers, post-processing filter after resolution

Usage

# Automatically detect and filter wildcards (replaces manual -wd)
echo "sub1.example.com\nsub2.example.com\nsub3.wildcard.com" | dnsx -aw

# Works with file input too
dnsx -l subdomains.txt -aw

Testing

  • go build ./... passes clean
  • New flag visible in -h output under Configurations group
  • Stream mode correctly rejected with error message

Known Limitations

  • getRootDomain() uses a simple two-label heuristic — works for .com, .org, etc. but not multi-part TLDs like .co.uk. Documented in code comments. A public suffix list library could be integrated in a follow-up if needed.
  • Single-probe detection per domain. Could be extended to multiple probes for higher confidence.

Summary by CodeRabbit

  • New Features
    • Added --auto-wildcard (-aw) flag to automatically detect and filter wildcard subdomains from enumeration results.
    • Tool now performs automatic wildcard detection before enumeration, identifies matching domains, and removes them from output.
    • Provides visibility into the number of wildcard subdomains filtered.
    • Note: Feature unavailable in stream mode.

Adds --auto-wildcard / -aw flag that automatically detects and filters
wildcard DNS domains across all input, similar to PureDNS.

How it works:
1. Before resolution, extracts unique root domains from all inputs
2. Probes each domain with a random subdomain (xid-generated)
3. Compares response IPs against root domain IPs
4. Domains returning the same IPs for random subdomains are marked
   as wildcard and their results are filtered from output

This eliminates the need to manually specify -wd for each domain,
making wildcard filtering practical for large multi-domain scans.

Changes:
- internal/runner/options.go: Add AutoWildcard bool field and -aw flag,
  block in stream mode (consistent with existing -wd behavior)
- internal/runner/wildcard.go: Add auto-detection logic with thread-safe
  domain tracking (RWMutex), root domain extraction, and per-domain
  wildcard probing
- internal/runner/runner.go: Integrate auto-detection before workers
  start, add post-processing filter for detected wildcard domains

Fixes projectdiscovery#924
@neo-by-projectdiscovery-dev
Copy link

neo-by-projectdiscovery-dev bot commented Mar 15, 2026

Neo - PR Security Review

No security issues found

Highlights

  • Adds --auto-wildcard / -aw flag for automatic wildcard DNS domain detection and filtering
  • Extracts unique root domains and probes each with random subdomain before resolution
  • Filters resolution results to exclude detected wildcard domains
Hardening Notes
  • Add rate limiting per root domain in wildcard detection to prevent resolver abuse
  • Sanitize error messages in non-verbose mode to avoid leaking DNS infrastructure details
  • Run go test -race to verify concurrency safety of wildcard map access patterns
  • Consider using golang.org/x/net/publicsuffix instead of two-label heuristic for proper .co.uk handling

Comment @pdneo help for available commands. · Open in Neo

@coderabbitai
Copy link

coderabbitai bot commented Mar 15, 2026

Walkthrough

The changes introduce automatic wildcard DNS detection across multiple domains. A new --auto-wildcard flag enables a detection phase that identifies wildcard-based DNS responses, then filters them from results automatically without manual domain specification.

Changes

Cohort / File(s) Summary
Configuration
internal/runner/options.go
Added AutoWildcard boolean field and registered --auto-wildcard flag with validation to reject usage in stream mode.
Execution Flow
internal/runner/runner.go
Integrated auto-detection phase before workers start and post-processing filter phase to remove wildcard-detected domains from results when enabled.
Wildcard Detection
internal/runner/wildcard.go
Implemented core detection logic with AutoDetectWildcards() method, global wildcard domain registry, IsAutoWildcardDomain() query function, and supporting helper methods.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Runner
    participant WildcardDetector as Wildcard<br/>Detector
    participant WildcardRegistry as Registry<br/>(global)
    participant Output

    Client->>Runner: Start with --auto-wildcard flag
    activate Runner
    
    alt AutoWildcard enabled
        Runner->>WildcardDetector: AutoDetectWildcards()
        activate WildcardDetector
        WildcardDetector->>WildcardDetector: Scan input domains
        WildcardDetector->>WildcardRegistry: Register detected wildcard domains
        activate WildcardRegistry
        WildcardRegistry->>WildcardRegistry: Store in global registry
        deactivate WildcardRegistry
        WildcardDetector-->>Runner: Return detection results
        deactivate WildcardDetector
    end
    
    Runner->>Runner: Process DNS resolutions<br/>(normal flow)
    
    alt AutoWildcard enabled & Wildcards detected
        Runner->>Output: Restart output worker
        activate Output
        Runner->>WildcardRegistry: Query IsAutoWildcardDomain()
        WildcardRegistry-->>Runner: Domain wildcard status
        Runner->>Runner: Filter & skip<br/>wildcard domains
        Runner->>Output: Write non-wildcard hosts
        Runner->>Output: Close channel
        Output-->>Runner: Worker done
        deactivate Output
    end
    
    Runner-->>Client: Results (filtered)
    deactivate Runner
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Wildcards once were hard to find,
Domain by domain, slow as mind.
Now auto-detection hops so fast,
Filtering fakes, no more a task!
One flag, one sweep—clean results at last! 🎉

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat: add automatic wildcard detection (--auto-wildcard)' directly and clearly describes the main feature addition, matching the core change in the changeset.
Linked Issues check ✅ Passed The PR successfully implements all three coding objectives from issue #924: automatic wildcard detection across multiple domains, automatic filtering of wildcard results, and exposure via the --auto-wildcard flag.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the --auto-wildcard feature; no unrelated code modifications or scope creep detected.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
internal/runner/wildcard.go (1)

11-13: Scope detected wildcard domains to Runner state.

This registry is package-global and survives for the lifetime of the process, so a second Runner in the same process inherits detections from the previous run. Keeping it on Runner (like wildcards) or clearing it at the start of AutoDetectWildcards() would avoid cross-run leakage and make tests more predictable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/runner/wildcard.go` around lines 11 - 13, The global registry
autoWildcardDomains and its mutex autoWildcardDomainsMutex leak state across
Runner instances; move this state into the Runner struct (e.g., add a
wildcards/autoWildcardDomains field and its mutex) and update
AutoDetectWildcards(), any callers, and checks to use r.autoWildcardDomains /
r.autoWildcardDomainsMutex (or clear autoWildcardDomains at the start of
AutoDetectWildcards() if moving is impractical) so each Runner has its own
scoped wildcard registry and tests/runs no longer inherit prior detections.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/runner/options.go`:
- Around line 312-314: Add a validation to reject using --auto-wildcard together
with --wildcard-domain: in the options validation block where
options.AutoWildcard is checked, also check options.WildcardDomain and call
gologger.Fatal().Msgf(...) if both are true, with a clear message like
"auto-wildcard and wildcard-domain are mutually exclusive"; reference the
options.AutoWildcard and options.WildcardDomain flags so the runner never allows
both (Runner.run executes manual wildcard post-processing and auto-wildcard
independently and will otherwise emit duplicate/inconsistent host output).

In `@internal/runner/runner.go`:
- Around line 559-594: The auto-wildcard filtering runs too late and only
post-processes already-emitted results; to fix, change the worker flow to buffer
outputs when r.options.AutoWildcard is true (same approach used for
options.WildcardDomain) so wildcard detection happens before any results are
written. Specifically, in worker() ensure startOutputWorker()/outputchan
buffering is enabled earlier when r.options.AutoWildcard is set, make the
auto-wildcard scan occur before closing outputchan, and have lookupAndOutput()
write into the buffered store (or reuse the same buffering mechanism) so
JSON/response-mode entries can be reconstructed and suppressed correctly; update
any related wait/close logic (wgoutputworker, close(outputchan)) to match the
wildcard-domain path.

In `@internal/runner/wildcard.go`:
- Around line 98-123: The wildcard detection currently only inspects DNSData.A
(in detectWildcardForDomain via r.dnsx.QueryOne and DNSData.A), causing failures
for non-A query types; update detectWildcardForDomain so that when auto-wildcard
is enabled it either (a) explicitly performs A-record lookups for the random
test subdomain and the root domain regardless of the user query type, or (b)
dynamically inspects the response field matching the requested record type
(e.g., DNSData.AAAA, DNSData.CNAME, DNSData.MX, etc.) instead of only DNSData.A;
change the comparisons and map construction (rootIPs and in.A iteration) to use
the selected record slice based on the active query type to correctly detect
wildcards for non-A queries.

---

Nitpick comments:
In `@internal/runner/wildcard.go`:
- Around line 11-13: The global registry autoWildcardDomains and its mutex
autoWildcardDomainsMutex leak state across Runner instances; move this state
into the Runner struct (e.g., add a wildcards/autoWildcardDomains field and its
mutex) and update AutoDetectWildcards(), any callers, and checks to use
r.autoWildcardDomains / r.autoWildcardDomainsMutex (or clear autoWildcardDomains
at the start of AutoDetectWildcards() if moving is impractical) so each Runner
has its own scoped wildcard registry and tests/runs no longer inherit prior
detections.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7f7e5963-bc4d-4dda-829a-f81cdb8b8649

📥 Commits

Reviewing files that changed from the base of the PR and between fe80b18 and 85f9e84.

📒 Files selected for processing (3)
  • internal/runner/options.go
  • internal/runner/runner.go
  • internal/runner/wildcard.go

Comment on lines +312 to +314
if options.AutoWildcard {
gologger.Fatal().Msgf("auto-wildcard not supported in stream mode")
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Reject --auto-wildcard together with --wildcard-domain.

While you're validating unsupported modes here, please also make those two flags mutually exclusive. Runner.run() executes the manual wildcard post-processing block and the auto-wildcard block independently, so enabling both re-emits surviving hosts a second time and produces inconsistent output.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/runner/options.go` around lines 312 - 314, Add a validation to
reject using --auto-wildcard together with --wildcard-domain: in the options
validation block where options.AutoWildcard is checked, also check
options.WildcardDomain and call gologger.Fatal().Msgf(...) if both are true,
with a clear message like "auto-wildcard and wildcard-domain are mutually
exclusive"; reference the options.AutoWildcard and options.WildcardDomain flags
so the runner never allows both (Runner.run executes manual wildcard
post-processing and auto-wildcard independently and will otherwise emit
duplicate/inconsistent host output).

Comment on lines +559 to +594
// Auto wildcard filtering - filter results from detected wildcard domains
if r.options.AutoWildcard && len(autoWildcardDomains) > 0 {
gologger.Print().Msgf("Starting to filter auto-detected wildcard domains\n")

// we need to restart output
r.startOutputWorker()

seen := make(map[string]struct{})
numRemovedSubdomains := 0

r.hm.Scan(func(k, v []byte) error {
host := string(k)
rootDomain := getRootDomain(host)

// Skip if this domain was detected as wildcard
if IsAutoWildcardDomain(rootDomain) {
if _, ok := seen[host]; !ok {
numRemovedSubdomains++
seen[host] = struct{}{}
}
return nil
}

// Output non-wildcard results
if _, ok := seen[host]; !ok {
seen[host] = struct{}{}
_ = r.lookupAndOutput(host)
}
return nil
})

close(r.outputchan)
// waiting output worker
r.wgoutputworker.Wait()
gologger.Print().Msgf("%d wildcard subdomains removed\n", numRemovedSubdomains)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

--auto-wildcard filters too late to affect the real output.

This block runs after Lines 475-476 have already closed the original output worker, so wildcard matches have already been printed/written once. Unlike the --wildcard-domain flow, worker() only buffers results when options.WildcardDomain != "", so --auto-wildcard never suppresses the first pass. The current behavior is an extra filtered pass appended to the unfiltered output, and lookupAndOutput() cannot reconstruct JSON/response-mode results because nothing was stored for this path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/runner/runner.go` around lines 559 - 594, The auto-wildcard
filtering runs too late and only post-processes already-emitted results; to fix,
change the worker flow to buffer outputs when r.options.AutoWildcard is true
(same approach used for options.WildcardDomain) so wildcard detection happens
before any results are written. Specifically, in worker() ensure
startOutputWorker()/outputchan buffering is enabled earlier when
r.options.AutoWildcard is set, make the auto-wildcard scan occur before closing
outputchan, and have lookupAndOutput() write into the buffered store (or reuse
the same buffering mechanism) so JSON/response-mode entries can be reconstructed
and suppressed correctly; update any related wait/close logic (wgoutputworker,
close(outputchan)) to match the wildcard-domain path.

Comment on lines +98 to +123
in, err := r.dnsx.QueryOne(testHost)
if err != nil || in == nil || len(in.A) == 0 {
return false
}

// If we got a response, query the root domain
rootResult, err := r.dnsx.QueryOne(domain)
if err != nil || rootResult == nil {
// Root domain doesn't resolve but subdomain does - likely wildcard
return true
}

// Check if the same IPs are returned (indicating wildcard)
rootIPs := make(map[string]struct{})
for _, a := range rootResult.A {
rootIPs[a] = struct{}{}
}

for _, a := range in.A {
if _, ok := rootIPs[a]; !ok {
// Different IP for random subdomain - not a wildcard at root level
return false
}
}

// Same IP returned - likely a wildcard
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n internal/runner/wildcard.go | sed -n '90,130p'

Repository: projectdiscovery/dnsx

Length of output: 1503


🏁 Script executed:

# Search for DNSData struct definition
rg -A 20 "type DNSData struct"

Repository: projectdiscovery/dnsx

Length of output: 47


🏁 Script executed:

# Search for DNSData more broadly
rg "DNSData" internal/runner/wildcard.go -A 2 -B 2

Repository: projectdiscovery/dnsx

Length of output: 47


🏁 Script executed:

# Check imports and structure of wildcard.go
head -40 internal/runner/wildcard.go

Repository: projectdiscovery/dnsx

Length of output: 1237


🏁 Script executed:

# Search for runner struct definition
rg -A 30 "type Runner struct"

Repository: projectdiscovery/dnsx

Length of output: 1942


🏁 Script executed:

# Search for query type configuration in runner.go
rg "QueryType|queryType|QueryRecord|Type" internal/runner/runner.go | head -20

Repository: projectdiscovery/dnsx

Length of output: 1197


🏁 Script executed:

# Get more context around the QuestionTypes configuration
rg -B 5 -A 10 "QuestionTypes = questionTypes"

Repository: projectdiscovery/dnsx

Length of output: 1031


🏁 Script executed:

# Search for detectWildcardForDomain function calls
rg "detectWildcardForDomain" internal/runner/

Repository: projectdiscovery/dnsx

Length of output: 316


🏁 Script executed:

# Get the call site for detectWildcardForDomain
rg -B 5 -A 5 "r.detectWildcardForDomain"

Repository: projectdiscovery/dnsx

Length of output: 757


🏁 Script executed:

# Search for AutoWildcard option handling
rg -B 3 -A 3 "AutoWildcard"

Repository: projectdiscovery/dnsx

Length of output: 3773


🏁 Script executed:

# Check how query types are determined based on options
rg -B 20 "QuestionTypes = questionTypes" internal/runner/runner.go | head -60

Repository: projectdiscovery/dnsx

Length of output: 676


🏁 Script executed:

# Get the full context of QuestionTypes building - go further back
rg -B 50 "QuestionTypes = questionTypes" internal/runner/runner.go | head -80

Repository: projectdiscovery/dnsx

Length of output: 1475


🏁 Script executed:

# Check if there's any special configuration when AutoWildcard is triggered
rg -B 10 "AutoDetectWildcards()" internal/runner/runner.go

Repository: projectdiscovery/dnsx

Length of output: 449


🏁 Script executed:

# Let's check what fields DNSData might have - search for field accesses on the response
rg "in\." internal/runner/wildcard.go

Repository: projectdiscovery/dnsx

Length of output: 402


🏁 Script executed:

# Search for imports to see what dnsx package is being used
head -20 internal/runner/runner.go | grep -E "import|dnsx"

Repository: projectdiscovery/dnsx

Length of output: 119


🏁 Script executed:

# Search in the repo for any type definitions or additional field accesses on response objects
rg "rootResult\.|in\." internal/runner/wildcard.go -A 1

Repository: projectdiscovery/dnsx

Length of output: 704


Non-A query types silently fail wildcard detection.

When --auto-wildcard is combined with non-A query type flags (-aaaa, -cname, -mx, etc.), detectWildcardForDomain() always checks only DNSData.A records (lines 99, 112-121), causing false negatives if the domain resolves for the specified record type but not for A records. The wildcard detection should either enforce A-record queries explicitly during auto-detection, or adapt to inspect the appropriate record type being queried.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/runner/wildcard.go` around lines 98 - 123, The wildcard detection
currently only inspects DNSData.A (in detectWildcardForDomain via
r.dnsx.QueryOne and DNSData.A), causing failures for non-A query types; update
detectWildcardForDomain so that when auto-wildcard is enabled it either (a)
explicitly performs A-record lookups for the random test subdomain and the root
domain regardless of the user query type, or (b) dynamically inspects the
response field matching the requested record type (e.g., DNSData.AAAA,
DNSData.CNAME, DNSData.MX, etc.) instead of only DNSData.A; change the
comparisons and map construction (rootIPs and in.A iteration) to use the
selected record slice based on the active query type to correctly detect
wildcards for non-A queries.

@khozakhulile27-netizen
Copy link

Implemented randomized wildcard probing to improve the accuracy of DNS wildcard detection. By using a unique prefix for each probe, we can bypass potential DNS caching issues and more reliably identify wildcard records.
​Changes
​Modified internal/runner/runner.go to use time.Now().UnixNano() for generating dynamic probe prefixes.
​Updated the host appending logic to use the format: aw-[timestamp].[domain].
​Cleaned up unused variables to ensure strict Go compiler compliance.
​Reasoning
​Static wildcard checks (like using "FUZZ") can sometimes be cached by intermediate DNS resolvers. Using a high-resolution timestamp ensure that every probe is unique, forcing the resolver to provide an authoritative response.

@flaggdavid-source
Copy link
Author

Thanks for the suggestion! We're using xid.New().String() which generates a globally unique ID per call (not static), matching the pattern already used in IsWildcard() in the same file. This ensures consistency with the existing codebase while still producing unique probes that bypass DNS caching.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support auto wildcard detection similar to PureDNS

2 participants