# 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` | `/` on the forge | | `repo` | `///...` | | `issue` | `///issues/` (or `pull`, `discussions`) | | `comment` | an issue or pull URL with `#comment-` | 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`, migrations `0045` and `0046`); **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 example `gitforstatic.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) 1. Enable Email Routing on the forge zone and verify the operator's destination address: `wrangler email routing addresses create you@example.com`. 2. Set the `ABUSE_FORWARD_TO` secret (`wrangler secret put ABUSE_FORWARD_TO`) to that address, so it stays out of the repo. The Worker forwards every abuse@ message there before filing reports, so nothing is lost if parsing fails. 3. After the Worker is deployed, route abuse@ to it: ```sh wrangler email routing rules create --name "abuse to worker" --match-type literal --match-field to \ --match-value abuse@ --action-type worker --action-value ``` 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.