bucket discovery: no name guessing

Remove all fabricated candidate names (suffixes and the wordlist). Discovery now
only tests names that actually occur in the incoming hostname: the subdomain
label and the registrable-domain label (assets.example.com -> assets, example).
Nothing is permuted or drawn from a wordlist.
This commit is contained in:
StasonJatham committed 2026-09-22 16:09:44 +02:00
1 parent ebb020c6b7
commit fe5c01666b
6 files changed
+56 -121

No files matched your search

+6 -52
View File
@@ -45,7 +45,6 @@ type Discoverer struct {
cfg Config
client *http.Client
q *domainQueue
names *domainQueue
tokens chan struct{}
seen *dns.TTLCache[bool] // candidate names already tested
stop chan struct{}
@@ -69,7 +68,6 @@ func New(cfg Config) *Discoverer {
cfg: cfg,
client: &http.Client{Timeout: 6 * time.Second},
q: newDomainQueue(cfg.Queue),
names: newDomainQueue(cfg.Queue),
tokens: make(chan struct{}, max(cfg.QPS, 1)),
seen: dns.NewTTLCache[bool](1<<20, 64),
stop: make(chan struct{}),
@@ -138,7 +136,6 @@ func (d *Discoverer) worker(ctx context.Context) {
case <-d.stop:
return
case <-d.q.notify:
case <-d.names.notify:
}
for {
host, ok := d.q.pop()
@@ -147,13 +144,6 @@ func (d *Discoverer) worker(ctx context.Context) {
}
d.scanDomain(ctx, host)
}
for {
name, ok := d.names.pop()
if !ok {
break
}
d.testName(ctx, name)
}
}
}
@@ -166,13 +156,6 @@ func (d *Discoverer) scanDomain(ctx context.Context, host string) {
}
}
// SubmitName tests a specific candidate bucket name (e.g. from a wordlist).
func (d *Discoverer) SubmitName(name string) {
if validBucketName(name) {
d.names.push(name)
}
}
func (d *Discoverer) testName(ctx context.Context, name string) {
if _, dup := d.seen.Get(name); dup {
return
@@ -272,30 +255,9 @@ func (d *Discoverer) test(ctx context.Context, t Target) {
// --- candidate generation ---
var suffixes = []string{
"", "-assets", "-static", "-backups", "-backup", "-images", "-img", "-media",
"-prod", "-dev", "-staging", "-test", "-files", "-uploads", "-public", "-cdn",
"-data", "-content", "-site", "-web", "-www", "-app", "-store", "-downloads",
"-photos", "-videos", "-docs", "-documents", "-archive", "-logs", "-artifacts",
"-build", "-deploy", "-release", "-db", "-database", "-sql", "-exports", "-reports",
"-assets-prod", "-assets-dev", "-static-prod", "-uploads-prod",
}
// Wordlist is a bounded set of common global bucket names tested once each.
func Wordlist() []string {
return []string{
"backups", "backup", "images", "image", "img", "media", "static", "assets",
"uploads", "files", "file", "data", "content", "public", "cdn", "www", "web",
"website", "app", "store", "downloads", "docs", "documents", "archive",
"logs", "artifacts", "build", "deploy", "release", "db", "database", "sql",
"dump", "exports", "reports", "photos", "videos", "dev", "test", "staging",
"prod", "production", "internal", "private", "secure", "userdata", "usercontent",
"thumbnails", "avatars", "profiles", "attachments", "tmp", "temp", "cache",
"config", "secrets", "keys", "certificates", "certs", "gifs", "png", "jpeg",
"download", "uploads-test", "uploads-dev", "assets-test", "assets-dev",
"static-assets", "web-assets", "app-assets", "site-assets", "media-assets",
}
}
// No name guessing: candidates are exactly the labels that occur in the incoming
// hostname (the subdomain label and the registrable-domain label). Nothing is
// fabricated, permuted, or drawn from a wordlist.
// baseLabels returns plausible bucket-name stems from a hostname: the leftmost
// label and the registrable-domain label.
@@ -325,17 +287,9 @@ var skipLabel = map[string]bool{
}
func candidates(host string, max int) []string {
var out []string
for _, b := range baseLabels(host) {
for _, s := range suffixes {
name := b + s
if validBucketName(name) {
out = append(out, name)
}
if max > 0 && len(out) >= max {
return out
}
}
out := baseLabels(host)
if max > 0 && len(out) > max {
out = out[:max]
}
return out
}
+4 -11
View File
@@ -5,16 +5,10 @@ import (
"testing"
)
func TestCandidates(t *testing.T) {
func TestCandidatesAreExactLabelsOnly(t *testing.T) {
got := candidates("assets.example.com", 200)
set := map[string]bool{}
for _, c := range got {
set[c] = true
}
for _, want := range []string{"example", "example-assets", "example-backups", "example-static"} {
if !set[want] {
t.Fatalf("candidates missing %q (got %v)", want, got)
}
if len(got) != 2 || got[0] != "assets" || got[1] != "example" {
t.Fatalf("candidates = %v, want [assets example] (no fabricated names)", got)
}
for _, c := range got {
if !validBucketName(c) {
@@ -24,7 +18,6 @@ func TestCandidates(t *testing.T) {
}
func TestBaseLabelsSkipsNoise(t *testing.T) {
// www is skipped, so only the registrable label remains.
got := baseLabels("www.example.com")
if len(got) != 1 || got[0] != "example" {
t.Fatalf("baseLabels = %v, want [example]", got)
@@ -57,7 +50,7 @@ func TestTargets(t *testing.T) {
}
func TestMaxPerDomain(t *testing.T) {
if got := candidates("example.com", 5); len(got) != 5 {
if got := candidates("example.com", 5); len(got) > 5 {
t.Fatalf("MaxPerDomain not honored: got %d", len(got))
}
}