Abuse handling
Reports land in one queue (abuse_reports, migration 0027_abuse_reports.sql) from three sources:
| Source | Entry point | Reporter identity (HMAC only) | Weight |
|---|---|---|---|
web |
/abuse form (submitAbuseReport, src/fns/abuse.ts) |
signed-in user, else client IP | user 2, IP 1 |
site |
POST /__gitfor/report on any hosted site |
client IP | 1 |
email |
mail to abuse@ routed to the Worker (src/server/abuse-email.ts) |
sender domain (addresses are free to mint) | 2 with aligned DMARC pass, 0 |
Every new report notifies the site admins. The /abuse form and the site endpoint share the REPORT_LIMITER
binding (5 per minute per IP). The site endpoint also requires an Origin header equal to the site's own origin, so
another page can't file reports in its visitors' names.
Email reports only count toward auto-pause when the topmost Authentication-Results header (prepended by
Cloudflare Email Routing) shows dmarc=pass aligned with the From domain. Unauthenticated mail is still filed for
review but has weight 0, since its sender is trivially forged.
Targets
Each report is linked to what its URL points at (resolveAbuseTarget, src/server/abuse-targets.ts):
| Kind | URL |
|---|---|
site |
site label, preview label, then a site custom domain |
user |
/<account> on the forge |
repo |
/<owner>/<repo>/... |
issue |
/<owner>/<repo>/issues/<n> (or pull, discussions) |
comment |
an issue or pull URL with #comment-<id> |
Repositories, accounts, issues, pull requests, comments, review comments and discussion comments have a Report
link that opens the form prefilled (#comment-, #review-comment- and #discussion-comment- anchors). The link is
hidden only from the account that owns the reported object; site and repository admins see it.
Reports about other hosts are kept but not linked.
Auto-pause
Only hosted sites auto-pause (sites.disabled_reason = 'Paused pending abuse review', sites.paused_at set, KV
meta republished so the dispatcher returns 451). Reporters are counted once per site (their highest weight) over
open reports from the last seven days:
| Site | Counted reporters | Any category | Phishing or malware |
|---|---|---|---|
| Scratch | weight >= 1 | 3 | 2 |
| Owned | weight >= 2 (signed-in users, DMARC-verified mail) | 20 (10 users) | 8 (4 users) |
Anonymous IPs never pause a claimed site; below the owned threshold an admin decides. Admins are notified on every auto-pause. Sites already disabled for another reason (expiry, repository deleted) keep that reason.
Review
Site admins (users.is_admin) get Site admin in the user menu:
/admin: users, orgs, repos, sites and abuse counters, organizations ranked by open reports, recent sign-ups./admin/abuse: the queue. Pause site / Keep paused / Restore site as before. Hide (repo, issue, comment, review comment, discussion comment) hides that single object from everyone but site admins (hidden_at, migrations0045and0046); Unhide reverses it. Acting closes every open report for the same target; Dismiss closes one report. Moderate by URL does the same without a report./admin/audit: every admin action (abuse decisions, hide/unhide, hosting suspend/resume, account disable/enable) with actor, target, reason and time (admin_audit_log).
A hidden repository answers 404 to non-admins on the web, API, MCP and git. Sites are taken down by pausing or
disabling the site. (The static Pages feature and its pages target kind were removed in migration 0049.)
Takedown latency
A pause, an account disable or a hosting suspension is a D1 write plus a KV write, but serving reads KV with an edge cache, so a takedown is not instant everywhere:
| What | Where | Lag |
|---|---|---|
| Site meta and slot pointers (pause, disable) | src/server/sites/dispatch.ts, cacheTtl: 30 |
up to 30 s |
| Hosting suspended / capped flags | ownerHostingAllowed, cacheTtl: 60 |
up to 60 s |
| Browser caches of site assets | cache-control: public, max-age=600 on non-HTML |
up to 10 min |
| HTML | max-age=0, must-revalidate |
next request |
| Avatar takedown check | per-isolate cache in avatarTakenDown |
up to 60 s |
KV itself is eventually consistent across locations, which can add seconds to a minute on top.
suspendHostingFor also asks Cloudflare to purge the cache of the account's hosts when CF_API_TOKEN (with Cache
Purge) is set (src/server/sites/cache-purge.ts): every Sites custom domain (Cloudflare for SaaS) through
CUSTOM_DOMAINS_ZONE_ID, and Sites labels (production, previews, PR aliases) under
SITES_DOMAIN through SITES_ZONE_ID. Worker responses are not normally stored by the zone cache, so this is
belt-and-braces; it cannot reach browser caches, which simply expire. Accounts disabled by an admin (reason
admin) also stop serving their repositories, including public ones, to everyone but site admins
(repoTakenDown in src/server/repos.ts), and so do their profile, activity, followers and avatar; a payment
dispute only makes them read-only. Avatar images already in a browser cache (immutable) can't be recalled.
Operator tasks
SITES_DOMAIN(for examplegitforstatic.dev) must be on the Public Suffix List before Sites, and scratch sites in particular, are opened to the public. See docs/sites-design.md "PSL submission".
Email setup (Cloudflare Email Routing)
-
Enable Email Routing on the forge zone and verify the operator's destination address:
wrangler email routing addresses create you@example.com. -
Set
ABUSE_FORWARD_TOinwrangler.jsoncto that address. The Worker forwards every abuse@ message there before filing reports, so nothing is lost if parsing fails. -
After the Worker is deployed, route abuse@ to it:
wrangler email routing rules create <zone> --name "abuse to worker" --match-type literal --match-field to \ --match-value abuse@<zone> --action-type worker --action-value <worker name>Until then a plain forward rule to the operator works, but no reports are filed and nothing auto-pauses.
What hiding removes
Hidden issues drop out of issue lists, search, hovercards, milestones, pinned and sub-issue lists, and their inbox notifications are deleted. Hidden comments drop out of the thread and of the parent's comment count (restored on unhide), and their authors can no longer edit them. Account exports keep everything.