Add a -tls-cert scan that actively dials the target's tls port and mines the
leaf certificate: SAN entries become candidate subdomains, and issuer/validity
feed posture flags (self-signed, expired, expiring soon, wildcard). Unlike the
passive crt.sh and certspotter feeds this probes the target directly, so it
surfaces certs never logged to a public ct log at the cost of touching the
target. Self-signed detection compares raw issuer/subject der and verifies the
cert against its own key rather than CheckSignatureFrom, which false-negatives
on leaf certs that omit CA key-usage bits.
Co-authored-by: vmfunc <vmfunc.lc@gmail.com>
* refactor(modules): extract parseYAMLModuleBytes from ParseYAMLModule
split the byte-parsing and validation core out of the file-reading
wrapper so module definitions can be parsed from memory. behavior is
unchanged: ParseYAMLModule reads the file then delegates.
* test(fuzz): add harnesses for parsers and extractors
cover the untrusted-input parsers that had no fuzz coverage: jwt
analysis and segment decode, js secret scanning, yaml module parsing,
openapi spec parsing, html title extraction and js endpoint extraction.
the secrets, openapi and endpoint harnesses assert invariants (match is
a substring of input, ok implies non-nil spec, results non-empty and
sorted); the rest are crash-only.
---------
Co-authored-by: vmfunc <vmfunc.lc@gmail.com>
the bare "Magento" body word matched any page that merely named the
platform, and "mage/" is a substring of the near-ubiquitous "image/"
mime type, so a migration guide carrying a single image reference
scored both signatures and cleared the detection threshold. key on the
frontend static-asset base, the data-mage-init widget attribute and the
module namespace, which only appear when magento actually rendered the
page.
Co-authored-by: vmfunc <vmfunc.lc@gmail.com>
jwt payloads are unpadded base64url, but the supabase detector decoded
them with RawStdEncoding, which errors on any payload whose base64 lands
on the url-safe - or _ characters and silently skips the token. mirror
the jwt.go decoder: raw base64url with a padded fallback. extract the
decode into parseSupabaseJwtBody so it is unit-testable off the network.
Co-authored-by: vmfunc <vmfunc.lc@gmail.com>
the manifest maps each route to an array of chunk paths, but the regex anchored on the opening bracket so only the first .js literal per array was captured, dropping the remaining chunks from the script list.
match each quoted chunk path instead, scoped to the relative static/ shape (literal or escaped slash) so non-chunk .js strings such as __rewrites destinations, which can be attacker-controlled absolute urls, are not pulled into the fetch list.
Co-authored-by: vmfunc <vmfunc.lc@gmail.com>
the wayback passive source used http while the crt.sh and certspotter
sources use https. passive results flatten straight into findings, so
an on-path attacker could tamper with the plaintext response to inject
or strip historical urls. use https to match the other two sources.
Co-authored-by: vmfunc <vmfunc.lc@gmail.com>
module execution built a bare http client, so -proxy, -H, -cookie and
-rate-limit applied to every other scanner but silently not to module
scans. route through httpx.Client so modules share the configured
transport like the rest of the run.
Co-authored-by: vmfunc <vmfunc.lc@gmail.com>
detect cloudflare waf, sucuri, incapsula, f5 big-ip asm and modsecurity
from the headers, cookies and block-page strings the waf itself sets, so
a site that merely names the vendor in prose does not trip them. grouped
in one file so the category can be reviewed or excluded as a unit.
* refactor(fingerprint): make the favicon tech table the single source
move the hash->tech map from internal/scan into internal/fingerprint
beside the hash function, exposed as LookupFaviconTech. the scan
Favicon path now resolves tech through it instead of a private map.
drop the demo-sync guard test: it existed only to keep scan's map in
sync with the yaml demo module, and that map no longer exists.
* feat(modules): name the matched tech in favicon evidence
favicon module findings now read the tech straight from the shared
fingerprint table, so a canonical hit reports "favicon mmh3=<n>
tech=<name>" instead of a bare hash. unknown hashes are unaffected.
* feat(config): add -config/-profile config file and profile overlays
extend the existing goflags yaml config mechanism (the same one -template
already uses) with an explicit -config path and named -profile overlays,
instead of adding a second config system.
resolveConfigInput unifies -config/-profile/-template into a single flat
yaml path for goflags to merge before Parse: unset, it returns "" and
goflags falls back to its ambient ~/.config/sif/config.yaml unchanged.
a profile overlays its keys onto the file's top-level keys in a go map
before handing goflags a flat temp file, so precedence stays explicit cli
flag > profile > file default > built-in default via goflags' own
DefValue sentinel merge. -config and -template share one config slot and
are mutually exclusive.
verifies empirically that goflags auto-creates and merges the ambient
config file with no explicit SetConfigFilePath call, which this feature
depends on.
* docs(config): document -config/-profile and drop the dead toml example
template-example.toml was unreferenced by any go file and did not match
how goflags actually reads config (flat long-name keys, yaml). replace it
with config-example.yaml showing the real schema, including a profiles
block, and document -config/-profile in the usage/configuration guides
and the man page.
* fix(config): validate malformed config on the no-profile path
resolveConfigInput used to return an explicit -config path unparsed when
-profile was not set, so a malformed yaml file skipped validation entirely.
goflags then silently discarded the decode error, dropping every real
setting in the file with no diagnostic and exit 0, while the same file with
-profile already errored cleanly through loadConfigMap.
route both branches through one buildFlatConfig that always loads via
loadConfigMap and only overlays a profile when one is selected, so a
malformed file errors the same way regardless of -profile.
* fix(config): let explicit cli flags override config and profile values
goflags' own merge (readConfigFile) treats a flag whose current value equals
its DefValue as "unset" and applies the config value over it. that makes an
explicit cli flag silently lose to the config file or a profile whenever the
user happens to pass the flag's own default, e.g. "-timeout 10s" against the
built-in 10s default: today the file's 1s wins even though the flag was set
explicitly on the command line. the same class of bug hits -threads,
-concurrency, -notify-severity, and any profile value on the same merge path.
scan the raw args for every flag the user actually passed (long or short
alias, space or "=" form) and strip those keys out of the resolved config/
profile map before it ever reaches goflags, so cli precedence holds
unconditionally instead of only when the value differs from the default.
flagAliasGroups derives the long/short alias groups from the real flag
registration (grouping by the shared flag.Value pointer) rather than a
second hardcoded name table, so it can't drift from registerFlags.
goflags also swallows a type-mismatched config value entirely (discards
fl.Value.Set's error); that is a separate, lower-severity gap in the
vendored dependency itself and is left alone, noted in a comment.
the probe title field is documented as an httpx-style page title, but
extractTitle returned the raw regex capture, so a title like
"Tom & Jerry" was reported verbatim instead of "Tom & Jerry".
run it through html.UnescapeString before trimming so the reported
title matches the rendered page.
Each has no way to observe cancellation, so a fan-out scanner (full
port sweep, directory brute-force) keeps draining its whole queue even
after ctrl-c or -max-time fires. EachCtx takes a context and stops
feeding workers once it's cancelled, while still handing the ctx to
the callback so an in-flight item can bail out too. Each is now a thin
wrapper over EachCtx with context.Background(), so existing callers
are unaffected until they opt in.
a module with a negative extractor group indexed matches[e.Group] past
the lower bound, panicking the executor goroutine and crashing the whole
scan. the existing bound only checked the upper end; check e.Group >= 0
too so an invalid group is skipped like an out-of-range one.
* fix(scan): tune js secret rules for false positives and missed formats
drop stripe pk_ publishable keys (public by design) and require a digit in
the generic secret value so camelCase identifiers stop tripping the entropy
gate. add stripe rk_ restricted keys, github fine-grained pat (github_pat_),
encrypted pem headers and version-anchored slack xapp tokens; drop the
trailing word boundary on the aws-secret and google rules so keys ending in
/ + or - still match.
renames the "stripe live key" rule to "stripe secret key", which changes
the rule label in the json findings output.
* feat(scan): detect more provider keys in javascript
add unique-prefix credential rules to the js secret bank: gitlab pat
(glpat-), anthropic api/admin keys (sk-ant-), npm tokens (npm_), google
oauth client secrets (GOCSPX-), stripe webhook secrets (whsec_), shopify
app tokens (shp[at|ss|pa|ca]_), sendgrid keys (SG.) and slack incoming
webhook urls. all ride the no-entropy slot since the prefix alone is
proof, so they carry near-zero false-positive risk.
* fix(logger): create log dirs and flatten url log filenames
CreateFile/Write kept the '/' from a target's url path when building the
log filename, so a target like http://host:port/ tried to open
<dir>/host:port/.log whose parent directory never existed. os.OpenFile
failed and scanTarget aborted the whole target before any scanning ran.
fold every '/' and '\\' run in the sanitized url into a single '_' via a
shared logPath helper so CreateFile and Write always resolve to the same
flat file, and mkdir the parent defensively in getWriter as a second line
of defense.
* fix(scan): route module status lines through the locking sink
subdomaintakeover, cloudstorage, and the next.js framework detector
printed their status/error lines with a bare fmt.Println straight to
os.Stdout, bypassing the output package's sink. under -concurrency>1
only the sink is lock-wrapped, so these lines could interleave or tear
against lines other targets write concurrently.
swap them for output.ScanStart / output.Error, matching every other
scanner in the package.
* fix(store): make snapshot filenames injective and writes atomic
sanitize() folds every separator run (and a literal '_') to one '_', so
distinct targets like https://a.com/x and https://a.com//x, or
host:8443/path and host_8443_path, sanitized to the identical string and
shared one snapshot file - the second target's Save silently clobbered
the first's baseline. Save's os.WriteFile also wasn't atomic, so two
targets racing on a collided path (or -concurrency>1 in general) could
interleave partial writes.
pathFor now appends 16 hex chars of the target's sha256 to the sanitized
name, so distinct targets never collide, and Save writes through a temp
file in the same dir followed by os.Rename so a reader always sees a
complete snapshot or the previous one, never a partial write.
this changes the on-disk snapshot filename scheme: existing users' -diff
baselines under the old sanitize()-only names won't be found and the
next run re-baselines from scratch (every finding reports as newly
added once). noting this as a deliberate tradeoff for a local hardening
pass rather than silently orphaning them.
* fix(store): check the deferred temp-file cleanup error
golangci-lint (errcheck) flagged the bare defer os.Remove(tmpPath) in
writeFileAtomic; wrap it so the discard is explicit.
* fix(js): stop the scanning spinner on early script-parse errors
htmlquery.Parse and QueryAll failures returned before spin.Stop(), leaving
the "Scanning JavaScript files" spinner running forever on those paths while
every other error branch in the function stops it first.
* fix(js): stop reporting quoted regex-flag literals as endpoints
Quoted regex flag combos like "/gi" or "/su" in ordinary JS (e.g.
str.replace("/gi", x)) matched the root-relative path alternative in
endpointRegex and were reported as endpoints. Denylist the known flag
bigrams by exact match instead of raising minEndpointLen, so real short
endpoints like /v1, /me, /ws still extract normally.
* fix(js): keep supabase findings from other projects on a per-project error
ScanSupabase iterates every JWT found in a script and can discover several
distinct supabase projects. Four error paths inside that loop (reading the
signup response, parsing it, fetching the openapi spec, and a missing
Paths field) returned nil and the whole error, discarding any results
already collected for earlier, successfully scanned projects. Skip the
broken project with continue instead so the scan still returns what it
found.
* test(js): drop flaky wall-clock redos timing probe
go's regexp is re2 and linear by construction; the timing threshold added
only flake under -race load, not coverage. the regex-flag-literal false
positive it sat next to is guarded by TestRegexFlagEndpointsNotReported.
* fix(detectors): let django csrf body field detect
csrfmiddlewaretoken is a hidden form body field django templates
render, never a header, so marking it HeaderOnly meant it could never
match and a real django form page (csrf field plus csrftoken cookie)
went undetected.
* fix(detectors): tighten aspnet, angular, astro and nextjs markers
* fix(frameworks): surface header version patterns to extraction
version extraction only ever searched the response body, so
header-shaped patterns like ASP.NET's "X-AspNet-Version: x.y.z" or
Flask's "Werkzeug/x.y.z" Server header could never match even when the
detector itself fired off that same header. add
ExtractVersionFromResponse, which also searches canonical header
lines, and point aspnet and flask at it. fix the aspnet header regexes
to match case-insensitively, since Go canonicalizes header names
("X-AspNet-Version" becomes "X-Aspnet-Version") and the old literal
pattern never matched the canonical form.
* test(detectors): pin fp/fn corpus for framework detection
promote the ad-hoc probe/sweep scratch tests used to find these
defects into a proper regression file: for each fix, assert both the
real-product positive still detects and the prose/other-product
negative does not, plus a sweep of unrelated pages against every
registered detector.
packaged installs put the binary in /usr/bin and the modules under
/usr/share/sif/modules, so the loader found zero built-in modules with no
error. add the freedesktop data dirs ($XDG_DATA_DIRS, default
/usr/local/share and /usr/share) as fallback search paths after the existing
executable-relative and working-dir locations, first-existing wins. the new
IsDir check also makes resolution stricter: a plain file named modules no
longer counts as the directory.
* fix(report): sort sarif driver rules for deterministic output
driver.rules was built by ranging a go map, so the emitted order
varied between runs over the same input, producing byte-unstable
sarif for identical scans. collect the rule ids into a slice and
sort them before building the rules list; ruleId still references
by id so no result is affected.
* fix(report): strip crlf from markdown target headers
target and module id are operator-supplied and were written verbatim
into "## " / "### " heading lines, so a target containing an
embedded newline followed by "## " text could inject a fake
standalone heading into the markdown report. sanitizeHeading
collapses cr/lf in both before they're written; the finding data
block itself was already unaffected.
* docs(scan): correct crawl's false robots.txt claim
the doc comment said robots.txt is respected because colly honors it
by default, but colly's default is IgnoreRobotsTxt=true and Crawl never
flips it, so Disallow rules were never enforced. state the actual,
intentional behavior instead: a recon crawler has no reason to treat
Disallow as a scope boundary.
pin the behavior with a regression test so it isn't "fixed" into a
partial robots.txt implementation by accident later.
* fix(scan): cap crawl request fanout
MaxDepth only bounds recursion depth; colly's own MaxRequests budget
was left at 0 (unlimited) and Crawl set no Limit rule, so a hostile
page fanning out to an attacker-controlled number of links got every
one of them fetched at that depth (b^d). cap the total number of
requests a single Crawl call will make with a fixed internal budget.
TestCrawl_BoundsRequestFanout proves a 2500-link fanout page no longer
drives the server past maxCrawlRequests+1 actual fetches.
* fix(scan): stop redirects from escaping crawl scope
Crawl called SetClient(httpx.Client(timeout)), which replaces colly's
whole *http.Client and discards the CheckRedirect colly installs to
re-check AllowedDomains on every redirect hop. with it gone, redirects
fell back to net/http's default: follow up to 10 hops to any host. an
in-scope page could 302 the crawler to an arbitrary off-scope host
(e.g. a cloud metadata endpoint) and it would be fetched.
colly's own AllowedDomains only matches on hostname anyway, which
would still let a same-host, different-port redirect through (an
internal service reachable on the same IP). build our own http.Client
on top of httpx's shared transport instead, with a CheckRedirect that
requires the redirect target to share the exact host:port of the
request that produced it, and caps the hop count ourselves since a
custom CheckRedirect drops net/http's own 10-redirect default.
TestCrawl_RedirectRejectsOffScopeHost proves the off-scope host is no
longer fetched; TestCrawl_RedirectFollowsSameHost proves ordinary
same-host redirects still are.
roundTripper.RoundTrip injects the global cookie, custom headers and
user agent on every hop, including redirect requests. because that
injection sits below the client's redirect handling, it undoes
net/http's own stripping of Cookie/Authorization/Www-Authenticate
when a redirect crosses hosts, so a target that 302s to an attacker
host would receive the scan's secret cookie and bearer token.
req.Response is only set on requests net/http built to follow a
redirect, and its Request field is the prior hop, so comparing hosts
there is enough to detect a cross-host hop without any extra state.
skip the sensitive header classes on that hop and keep injecting
everything else as before.
* feat(output): add routable Sink for per-scan output capture
introduce a Sink (writer plus an interactive flag) that the package-level
chrome funcs delegate to via DefaultSink, and give ModuleLogger a sink it
writes through. a caller can now hand a scan its own Sink so that scan's
chrome lands on a dedicated writer instead of the process-wide default.
default routing is unchanged: Info/Warn/Module and friends still write to
the same stdout/stderr sink SetSilent configures.
* fix(output): trim redundant sink doc comments
drop comments on thin package-level wrappers that just forward to
the Sink methods, and tighten the Sink methods' own comments to
note the api-mode no-op instead of restating the func name
the google-search client only swaps spaces for '+' and drops the term
into the query string verbatim, so a dork carrying a raw '#' or '&' cut
the request url short at the fragment or split it into stray query
params. the search then ran against a fragment of the intended dork.
encode the term up front so the whole dork survives.
truncateBanner sliced by byte offset, so a multibyte utf-8 rune landing
across the limit was cut mid-sequence and emitted invalid utf-8. shodan
banners routinely carry non-ascii bytes, so this corrupted logged and
printed output. count runes instead so the cut lands on a boundary.
* fix(scan): honour the caller context when downloading the ports list
the common-scope ports fetch built its request with context.TODO(),
so a cancelled run or -max-time deadline could not abort a hung list
download even though the dialer already used the passed context.
* test(scan): drop redundant comment restating release channel receive
GetPagesRouterScripts fetched _buildManifest.js with httpx.Client(0),
which sets no client timeout, so a slow or hostile manifest host could
hang the whole scan on the response read. thread the caller's scan
timeout through instead, matching every other fetch in the package.
the wayback line scanner capped each line at 1mb, so a single archived
url with a huge query string made scanner.Err return ErrTooLong and the
whole feed was discarded. read lines without a per-line cap, still bounded
by the passiveMaxBytes read limit.
drupal's generator meta only exposes a bare major, so the extracted
version is "10"/"9". versionAffected only matched when the affected
entry was a component-prefix of the version, never the reverse, so a
bare major never matched a dotted affected entry and every drupal cve
was silently missed. match on dotted boundaries in either direction.
modules loaded only from an exe-adjacent modules/ dir or ~/.config, so a
bare `go install`ed binary shipped zero modules. embed the modules/ tree
and fall back to it when no filesystem builtin dir is found.
the on-disk builtin dir and the user-override dir still take precedence,
so a dev tree and a release that ships the folder alongside the binary
keep working unchanged. the embed directive lives at the repo root
because go:embed cannot reach a parent directory, and it registers the
filesystem with the loader through a package hook to avoid an import
cycle back into the root package.
sarif and markdown were the only structured exports and both drop the
per-item detail the normalized finding model already holds. add a -json
flag that writes the run's findings as a json array, one object per
finding with its target, module, severity, key, title and evidence.
carry detection confidence onto the finding model too so framework
detections report their score. it rides into the json report and is
omitted for scanners that carry none. the -silent line format and the
snapshot on-disk shape stay unchanged, since the report uses a dedicated
json view rather than marshalling the finding struct directly.
sarif results were hardcoded to level "warning", so a critical finding
and an informational one landed identically in code-scanning ingests.
carry the normalized severity onto report.Result and map it: critical
and high to error, medium to warning, low and info to note. the severity
is derived by flattening each module result and taking the worst rank,
and an absent severity keeps the old "warning" default.
numericClaim only accepted exp as a json number, but some emitters
serialize it as a quoted string. a token with a past string exp was
reported as missing exp instead of expired, an expiry false negative
and a missing-claim false positive on the same token. also parse a
string value via strconv.ParseFloat, falling back to absent when it
does not parse.
hsts grading was gated on the scheme of the originally requested url,
but the client follows redirects, so an http target that redirects to
https skipped the hsts check entirely and dropped a high-severity
finding. decide the scheme from the final response request url instead,
falling back to the requested url only when no response request is set.
paths was typed as a map of operation structs, so a spec with a $ref
string path item (valid in openapi 3.1) failed to decode and the whole
spec was dropped with no finding. decode path items into a generic tree
and extract per-operation security manually, treating a non-array
security value as absent so it inherits the global default rather than
being reported as an anonymous endpoint.
* feat(modules): tcp module executor
dial+probe+banner with word/regex/size matchers, ctx-honest watcher,
port required.
* feat(modules): add redis unauthenticated access tcp module
first shipped tcp module: sends INFO to redis (6379) and matches
redis_version in the reply, which an auth-required server never returns
(it answers -NOAUTH). extracts the leaked version.
* docs(modules): document the tcp module type
flip the http-only note and add a tcp config section (port, data,
word/regex/size matchers and regex extractors against the banner),
pointing at the redis demo module.
* feat(modules): support matchers-condition in tcp modules
tcp matchers were and-only; add the matchers-condition key (and default,
or) mirroring the http executor's checkMatchers, validated at load via the
shared validateMatchersCondition. documents the override in the combining
matchers section.
* feat(modules): decode escape sequences in tcp data payloads
decode C-style escapes (\\ \a \b \f \n \r \t \v \xHH) in a tcp module's
data value before it goes on the wire, so a single-quoted or plain yaml
scalar reaches the service with the same control bytes a double-quoted
one already would. an unrecognized escape is kept verbatim so no bytes
are silently lost.
* feat(modules): add tcp recon modules for common services
add five built-in tcp modules alongside the redis one so the executor
ships a real service set: memcached (unauth version probe), ssh and ftp
(passive banner version disclosure), mysql/mariadb (handshake exposure,
matched with matchers-condition: or across both auth plugin names), and
vnc (rfb handshake exposure). each fingerprint tracks the documented
on-wire protocol and pulls the version into an extractor.
* feat(modules): add mail and rsync tcp recon modules
add four more passive-banner tcp modules: smtp, pop3, and imap read the
greeting each mail service sends on connect (the smtp matcher requires
an esmtp/smtp marker so it does not fire on a bare 220 ftp greeting),
and rsync detects an exposed daemon by its @RSYNCD handshake. each pulls
the banner or protocol version into an extractor.
pointsAtSentinel parsed the reflected location without the whitespace
and control-byte removal a browser applies, so payloads like a
space-prefixed scheme-relative host or a tab embedded in the scheme
parsed to an empty host and a real off-site redirect was missed. strip
leading and trailing c0-or-space and all tab, cr and lf before parsing,
matching the navigation-time normalization, without removing mid-string
spaces so a same-site target is never reclassified as off-site.
the http scheme downgrade origin was queued against every target, so on
a plain-http target the http://host probe is the target's own origin and
any correct same-origin reflection was reported as a high-severity
downgrade. mark that origin https-only and skip it when the target
scheme is not https, where a downgrade is meaningless, while still
firing it on https targets that genuinely trust the http origin.
checkDatabaseErrors matched db error regexes against each response with
no baseline, so a site serving a shared error template, custom 500 or
waf block page containing a db error string reported an identical
high-severity sql injection finding on every probed path. capture the
main-url response as a baseline and skip, per pattern, any error string
already present in it while still checking other patterns, so only
differential probe-caused errors are reported.
the lfi scan matched evidence regexes against each response with no
baseline, so a page whose static content matched a pattern (a blog
containing an /etc/passwd-shaped line, a code sample with <?php) was
reported as an lfi finding for every parameter and payload combination.
take one baseline get of the unmodified target and exclude, per
evidence class, any pattern already present in it, so only
payload-caused disclosures are counted.
* feat(modules): add haproxy stats, nexus and pushgateway exposure modules
* chore(modules): trim redundant module header comments
drop the top-line comments on the new infra exposure modules that
just restated the id/name fields already in the info block
* feat(modules): add chartmuseum and verdaccio registry exposure modules
* chore(modules): trim redundant module header comments
drop the top-line comments on the new package registry exposure
modules that just restated the id/name fields already in the info block
* feat(modules): add tfvars, ansible-vault and ci config exposure
* chore(modules): trim redundant module header comments
drop the top-line comments on the new iac config exposure modules that
just restated the id/name fields already in the info block
* feat(modules): add guacamole and webmin login exposure modules
* chore(modules): trim redundant module header comments
drop the leading comment restating id/name on guacamole-login-exposure and webmin-login-exposure
* feat(modules): add nsq and pulsar exposure modules
* chore(modules): trim redundant module header comments
drop the leading comment restating id/name on nsq-stats-exposure and pulsar-metrics-exposure
detect a reachable postfixadmin setup.php (unhardened-install signal
and info disclosure, not an auth bypass since superadmin creation is
gated on a locally configured setup password) and the login panel.
markers and the setup-password form field and login field pair with
the brand string so prose mentions do not match.
fingerprint roundcube via its login-page body markers and zimbra via
its web client markers, both anded with status 200 and prose-trap
negatives to avoid matching pages that merely mention the products.
detect an anonymously reachable kubeflow pipelines apiserver, whose
same api accepts pipeline run submission with an attacker-supplied
workflow manifest (arbitrary container execution), and a metaflow
metadata service leaking flow and owner enumeration. match distinctive
snake_case json keys with status 200, fail closed on empty instances
to avoid the bare-substring false-positive class.
detect anonymously reachable localai /system (backend and loaded-model
disclosure) and invokeai /api/v1/app/runtime_config (host path and
config disclosure). both and json key markers with status 200 and
include secured-instance negatives so 401/403 boxes are not flagged.
require actual autoindex markup (parent-directory anchors, sort links,
bracketed parent link) instead of loose title text, so prose pages
titled "Index of /..." and iis tutorials no longer false-positive.
apache non-fancy parent-directory anchor tolerates surrounding
whitespace to avoid a false negative.
the live-response path flagged any host whose body matched a provider
fingerprint without checking dns, so a page merely containing a
provider 404 string was reported as a takeover. resolve the cname and
only confirm when it maps to the same provider; a contradicting cname
kills the flag. providers with a body signature but no apex to
correlate, and unresolvable lookups, degrade to a medium potential
finding via a new confidence field rather than confirming or dropping.
Detect an exposed Laravel Telescope dashboard, which reveals requests, queries,
exceptions and dumped variables. Telescope aborts with 403 before render when
gated, so a 200 carrying both the Telescope title and the Vue mount id proves
the gate is open. The and-condition on the mount id keeps an unrelated page that
merely names telescope, or a soft-404, from matching.
Detect unauthenticated web database browsers, which ship with no default auth
and hand over a full data browser once reachable. Each keys on an app-specific
anchor (the ME_SETTINGS global, the redisCommander.js script, ace-pgsql.js)
under an and-condition so a branded soft-404 cannot trip it. mongo-express also
requires the database-list heading, since its login form renders the same
settings global, title and logo but serves no data.
* feat(modules): add host-header injection and xxe probe modules
host-header-injection sets an rfc 2606 .invalid marker in the forwarded-host
headers and fires only when it comes back inside an absolute-url construct
(href/src/action/content/url), so a benign echo into text or an attribute cannot
trigger it. xxe-error-based expands a SYSTEM entity pointing at a local file and
matches only that file's contents, so an echo of the payload (a path string)
cannot satisfy it.
* chore(modules): trim redundant module header comments
drop the leading comment restating id/name on host-header-injection and xxe-error-based
A module that fingerprints a redirect itself (open-redirect proofs, a Location
header, a 3xx status) could not see the redirect: the executor followed 3xx to
the final response before matchers ran, with no way to stop. Add an opt-in
disable-redirects field that scopes an ErrUseLastResponse policy to the module's
own client copy, so the shared httpx transport keeps following redirects for
every other module.
* feat(modules): add active web-vuln probe modules
Add five active-probe modules to modules/http: ssti-reflected (arithmetic
eval proof), command-injection (id output, not payload echo), and error-based
xpath, nosql, and ldap injection. Each keys on a transformed response the
payload cannot produce by reflection, so a benign echo cannot trigger it.
* chore(modules): trim redundant module header comments
drop top-line comments that just restate the id/name fields already
present in the same file
Crawl only capped recursion depth, not breadth per level. A link-heavy page
(pagination, faceted search) drives fetch count toward branching^depth with no
ceiling: a depth-3 crawl of a page linking 40 fresh urls issued 1641 real
fetches.
Cap total fetches at a page budget via an atomic counter in an OnRequest hook
that aborts once exceeded, and add a Truncated flag on CrawlResult so callers
can tell a run was cut short.
* refactor(cli): extract per-target scan into scanTarget
pull the target loop body into scanTarget, which returns a targetScan
holding that target's findings, report rows, scan labels and log files
instead of appending onto run-wide slices. the run loop merges those in
target order and finishRun handles the post-loop notify/silent/report/
summary steps. behavior is identical for the sequential run today; the
isolation is the seam a bounded worker pool needs next.
* feat(cli): scan targets in parallel with -concurrency
add -concurrency N (default 1) to run the per-target scan pool over
several targets at once. scanTarget already returns isolated
accumulators, so workers share only the console: output.SetConcurrent
serializes sink writes and de-animates spinners/progress so parallel
lines stay whole. results merge in input order, and concurrency 1 keeps
the sequential path unchanged.
add recon modules for container and proxy control planes that answer without authentication: traefik serves its full routing config at /api/overview when the api is enabled, nomad dumps the agent config at /v1/agent/self when acls are disabled (403 otherwise), and portainer discloses its version and instance id at the public /api/status.
add recon modules for unauthenticated local inference servers that leak model inventory without a key: ollama, koboldcpp, text-generation-webui (oobabooga), and tabby.
add recon modules for unauthenticated inference servers that expose an openai-style /v1/models inventory without a key: vllm, llama.cpp, lm studio, and infinity.
add recon modules for self-hosted databases whose http interface is reachable
without credentials: clickhouse runs arbitrary sql because the default user has
an empty password, confirmed here by reading the server version through the
http interface, and the open-source dgraph alpha has no authentication so its
/health endpoint discloses the cluster while /query and /admin read and drop
all data; a clickhouse that requires a password returns 403 and an alpha behind
an authenticating proxy returns 401 and neither is flagged.
add recon modules for self-hosted agent builders and compute
orchestration. ray exposes an unauthenticated compute dashboard whose
job api allows code execution, skypilot exposes an open cloud and
kubernetes control plane when basic auth is disabled, dify flags a
console that allows open registration, and langflow fingerprints a
reachable instance over its public version api.
the shared transport paced outbound requests but ignored a server
asking it to slow down, so a 429 or 503 came straight back as a failed
probe. back off and retry both through the one chokepoint every scanner
shares, honoring Retry-After (delta-seconds or http-date) and falling
back to capped exponential backoff.
gated behind -max-retries (default 2, 0 disables). bodyless GETs and
GetBody-backed requests replay safely; anything else is returned as-is.
add two recon modules detecting anon-by-design service registries that leak the internal service map:
- eureka-registry-exposure (medium): probes /eureka/apps and matches the registry envelope markers apps__hashcode and versions__delta (the EurekaJacksonCodec formats them with a default '_' -> '__' replacement, so they appear verbatim in both the xml and json forms); eureka has no auth, so an open registry discloses every instance's internal hostname, ip and ports and accepts rogue registrations, while a spring-security-secured eureka returns 401; extracts the first instance ip from either form
- spring-boot-admin-exposure (medium): probes /instances and matches the registration/healthUrl/statusInfo shape; the codecentric server ships no security, so an open one discloses every registered app's internal management and health urls (a pivot to their actuators), while a secured one returns 401 or redirects to login; extracts the first internal health url
both modules carry hard-test coverage: eureka is proven on both its xml and json forms, with an N-1 trap (an apps__hashcode-less envelope stays quiet) and a prose trap (a page that merely mentions the word applications stays quiet); sba has an N-1 statusInfo trap; both have 401 secured-instance cases and cross false-positive guards both ways.
add two recon modules detecting anon-by-design big-data control surfaces that leak operational topology:
- zookeeper-admin-exposure (medium): probes /commands/monitor on a zookeeper adminserver and matches the command/monitor envelope plus server_state; the adminserver has no auth on reads, so an open one discloses the version, ensemble role, connection counts and data size, while a firewalled or disabled adminserver is not reachable; extracts the version
- hadoop-namenode-exposure (medium): probes /jmx for the NameNodeInfo bean and matches the bean name plus LiveNodes/DeadNodes; the namenode jmx is unauthenticated unless kerberos spnego is set, so an open one discloses the hdfs version and every datanode internal hostname, while a kerberos-secured namenode returns 401; extracts the software version
trino/presto were dropped: /v1/info is ResourceSecurity(PUBLIC) so it answers on secured clusters too and would be a fingerprint, not an exposure.
both modules carry hard-test coverage with N-1 anchor traps (a non-monitor zookeeper command and a non-NameNodeInfo hadoop bean stay quiet), cross false-positive guards both ways and a secured-instance status case.
add two recon modules detecting exposed kafka management consoles that ship without authentication by default:
- kafka-ui-exposure (high): probes /api/clusters on a kafbat/provectus kafka ui and matches the kafka-ui-specific defaultCluster/brokerCount keys plus the online/offline/initializing status enum; an open instance leaks the cluster topology and lets an unauthenticated session browse and produce records, while a login- or oauth-protected instance returns 401 and is not flagged
- kafdrop-exposure (high): probes / with an Accept: application/json header and matches the ClusterInfoVO summary/preferredReplicaPercent/brokers/topics shape; an open instance discloses broker hostnames, topics and messages, while an instance with the optional basic auth returns 401
both modules carry hard-test coverage with cross false-positive guards both ways, non-enum-status and missing-key negative cases, a wire-level check that the kafdrop module sends the Accept header, and 401 secured-instance cases
add a recon module for an n8n instance that serves /rest/settings without
authentication; the endpoint exists so the frontend can load but it discloses
the webhook url structure, instance id, release channel, configured
authentication method and whether owner setup is still pending, which aids
reconnaissance and can indicate a claimable instance; it is unauthenticated by
design so this is reported as information disclosure.
add a recon module for a grafana with anonymous access enabled: by default
grafana requires a login and /api/search returns 401, but when anonymous
access is turned on the endpoint lists every dashboard without credentials,
exposing the internal metrics, hostnames and queries they contain, and the same
anonymous session can reach backend data sources through the data source proxy;
a grafana that requires login returns 401 and is not flagged.
add recon modules for self-hosted automation servers whose api is reachable
without credentials when misconfigured: jenkins /api/json answers when
anonymous read access is enabled and lists every job, view and node, and
apache nifi /nifi-api/flow/about answers when the instance runs without
security; each open instance reaches a script console or processor that runs
arbitrary code, while a jenkins without anonymous read returns 403 and a
secured nifi returns 401 and neither is flagged.
add a recon module for an unauthenticated prometheus alertmanager: the
/api/v2/status endpoint answers without credentials and returns config.original,
the full running configuration, which discloses receiver integrations and any
embedded credentials such as slack, pagerduty and webhook urls and smtp
passwords, along with the build version and cluster peers; an instance behind an
authenticating proxy returns 401 and is not flagged.
add recon modules for self-hosted browser-automation grids that do not
authenticate their status endpoint: selenium grid /status discloses every
registered node with its os and browser slots, and selenoid /status discloses
the running sessions, capacity and browser images; each open grid also lets
anyone create a session that drives a real browser to arbitrary internal urls,
while a grid behind an authenticating proxy returns 401 and is not flagged.
add recon modules for self-hosted background-job dashboards that ship no
authentication of their own and rely on the hosting application to protect
them: sidekiq web /sidekiq/stats discloses the redis server internals and the
job queue, celery flower /api/workers (reachable only when its api is
deliberately opened) discloses every worker's broker config and registered
tasks, and rq-dashboard /0/data/queues.json discloses the redis-backed queue
names and job counts; each open instance also allows killing, retrying or
deleting jobs, while a deployment protected by the application returns a
redirect or 401 and is not flagged.
add recon modules for self-hosted data-orchestration webservers that ship no built-in authentication: dagster /server_info discloses the webserver and core versions, and mage /api/status discloses the scheduler status and server repository path; both reach an editor or graphql api on the same instance that can execute arbitrary code.
add recon modules for unauthenticated proxy and server admin interfaces that should be loopback-only: caddy /config/ returns the full running configuration and accepts a config-replacing post, and envoy /server_info exposes the build and command-line options of an admin interface that also offers config_dump and shutdown.
add a recon module for an exposed node-red admin api; with adminAuth unset the /flows endpoint returns the flow configuration without a token and the open admin api allows flow deployment that runs arbitrary code through function and exec nodes, while a secured instance returns 401.
add recon modules that fingerprint ci/cd pipeline servers via their public version endpoints: concourse /api/v1/info, woodpecker /version (keyed on its source repository) and gocd /go/api/version (keyed on its gocd commit url); each answers without authentication and discloses the server version.
add recon modules for unauthenticated observability backends that leak service topology and log data: loki serves its log query api when auth_enabled is false (401 otherwise), and jaeger and zipkin ship no authentication so a reachable instance exposes the service map and trace data.
add a recon module for the one-api / new-api self-hosted llm gateway,
whose public status endpoint discloses the system name, version, and
quota configuration.
add recon modules for self-hosted speech inference servers that are exposed without authentication: speaches (faster-whisper successor, speech-to-text and text-to-speech) keyed on its non-openai model task field, and xtts-api-server keyed on its get-folders endpoint that discloses local filesystem paths.
add recon modules for self-hosted vector and semantic search engines
reachable without auth: marqo, vespa, and meilisearch each allow
unauthenticated read and write of the indexed data, and their root or
version endpoints disclose the build.
add recon modules for self-hosted training and experiment-tracking
platforms reachable without auth: mlflow, tensorboard, aim, and
determined disclose experiments, the artifact store, training run paths,
and cluster topology over unauthenticated apis.
add recon modules for self-hosted image generation servers reachable
without auth: comfyui, automatic1111, fooocus-api, and iopaint each
expose unauthenticated generation or editing and disclose the installed
models.