Watchtower is a single-operator bug-bounty monitoring bot: a small allowlist of Telegram users drives it, it watches public bug-bounty targets, and every outbound request is filtered through a default-deny scope engine before a single packet leaves the Worker.
- Webhook secret —
/telegramrequests must carryTELEGRAM_WEBHOOK_SECRET(Telegram'sX-Telegram-Bot-Api-Secret-Tokenheader or a?secret=parameter), otherwise the request is rejected with 401 before the body is parsed. - Telegram allowlist — every command except
/startrequires the sender's id inAUTHORIZED_TELEGRAM_IDS(env) ∪allowed_users(D1, managed with/allowand/disallow). Anyone else gets exactly one reply:⛔ Unauthorized. Contact your Watchtower administrator. - No anonymous surface — there is no REST API, no unauthenticated route
besides
/and/health(which return static JSON only).
The scope engine (src/scope/match.ts) is the only gate between "the operator
typed a name" and "we touch a host":
- Default deny — nothing is allowed unless an allowlist row matches.
/add example.comwrites the rootdomainrow, which covers every subdomain (no separate scope declaration). - Deny beats allow —
/excluderows (subdomain,*.wildcard, path) win over any allowlist match, andcheckHostInScope()/checkUrlInScope()run before every network request: probes, JS fetches, fuzz requests, bruteforce lookups, and the extra-port rotation all consult it first. - Hard-blocked regardless of scope — private/loopback/link-local/multicast
ranges, IPv4-mapped equivalents, cloud metadata endpoints
(
169.254.169.254,metadata.google.internal, …), reserved documentation domains (example.com,.test,.invalid, …), and over-broad wildcards (*,*.com). - Redirects are re-validated — a 30x target must pass
checkUrlInScope()again (validateRedirect()), so an in-scope host cannot bounce us to a third party. - No authorization workflow — v4 targets are public bug-bounty programs by
design; monitoring runs until
/remove. The operator is responsible for only adding programs they are authorized to test (see Responsible use).
Outbound HTTP goes through safeFetch() (src/security/ssrf.ts):
- Scheme allowlist (
http:/https:only) and in-scope URL check. - Hostname resolved via DNS-over-HTTPS before the fetch; the request is
refused if any resolved IP is private/reserved/metadata (range masks are
compared unsigned, so e.g.
192.168/16and169.254/16match correctly; IPv4-mapped IPv6 like::ffff:10.0.0.5is unmapped and checked as IPv4). AuthorizationandCookieheaders are stripped from outbound requests.- Response size is capped by streaming with an early abort
(
LIMITS.MAX_RESPONSE_BYTES), plus a per-request timeout. - The fuzzer stops the whole host chunk on HTTP 429 (
stopOn429), and Cloudflare's own runtime blocks private egress as defense-in-depth.
- Detected secrets are never stored in plaintext — only salted SHA-256
fingerprints (salt =
REDACTION_SALT) for deduplication. redactSync()scrubs notification payloads before they are written to thenotificationstable; Telegram messages escape all interpolated strings as HTML.- Pattern coverage: AWS/GCP/Azure/GitHub/GitLab/Slack/Stripe/Google/Twilio/ SendGrid/Mailgun/OpenAI/npm credentials, JWTs, bearer tokens, connection strings, webhook URLs, and whole PEM private-key blocks (the entire block is replaced, not just the header line).
- Requests are GET/HEAD only; no exploitation, no credential testing, no form submissions, no state-changing verbs.
- Bounded everything: chunked wordlists, probe rotation, wall-clock deadline
per pass,
stopOn429, per-target D1 locks — scanning stays polite and inside the Workers free tier. - The bot never executes shell commands, never runs operator-supplied regex, and never touches an excluded host.
REST API + bearer tokens, HMAC webhook signatures, external scanner runners, Slack/Jira/GitHub/email integrations, encrypted evidence (R2), audit logs, emergency stop, RBAC/multi-tenancy, and per-target authorization records.
Open a private report through the program the repository is published under, or contact the maintainer directly. Please include reproduction steps and impact; do not file public issues for security bugs.