Threat model — v1
A short, concrete document describing who shithub defends against,
what we protect, and how the controls map to attackers. This is the
v1 baseline; it evolves with the codebase. Per the S35 spec, "two
or three pages, not thirty." Pair with security-checklist.md for
the per-control test references.
Assets
In rough decreasing order of consequence if compromised:
- User credentials. Password hashes, TOTP secrets, recovery codes, PATs, SSH public keys. Direct path to account takeover.
- Repository content. Source code, issues, PR discussions, private-repo files. Privacy + IP value; for some users, source IS the product.
- Webhook secrets + delivery payloads. A leaked webhook secret lets an attacker spoof events to subscriber systems. Payloads often contain user content that hadn't been published.
- Site-admin actions. Suspending users, deleting repos, impersonating accounts. Trust violations here cascade.
- Server-side resources. CPU (argon2 hashing, git ops), disk (repo storage, response-body cap), DB connections.
Attackers
A1 — Compromised user account
The attacker authenticates as a real user (phished password, leaked PAT). They have whatever the user has.
Mitigations:
- Per-account session-epoch invalidation: the user can "log out everywhere" and burn every session at once.
- TOTP 2FA gate (S06) — when enabled, the password alone doesn't log in. Recovery codes one-shot.
- Audit log on security-relevant actions (auth, settings, admin) surfaces the attack to the user + admins.
- Suspended-account gate (
policy.Can) lets an admin freeze a hijacked account without nuking the user's data. - Common-password blocklist + argon2id make password-spraying impractical.
A2 — Malicious public-repo viewer
An anonymous or freshly-signed-up user crafts payloads against the public-repo surface: XSS via issue body, CSRF from a public-org page, SSRF via a webhook to internal infrastructure.
Mitigations:
- All user content is markdown-rendered through
internal/markdownwithbluemondayUGC policy; ad-hoctemplate.HTML(...)is lint-blocked outside designated render helpers. - CSRF protection at the router root (nosurf-derived); state- changing routes inherit it. PAT-only API and git transports are explicit exemptions documented in the lint script.
- SSRF defense (
internal/security/ssrf) on every outbound HTTP: IP block-list + dial-the-IP transport defeats DNS rebinding. - Webhook secret-decrypt failure auto-disables the hook so a compromised key doesn't keep delivering forever.
- CSP, Frame-Options DENY, COOP, CORP cut off the embed/clickjack surface.
A3 — Abusive signup automation
Botnets create accounts in bulk to spam issues, host abuse content, or burn through resources.
Mitigations:
- Per-IP signup throttle (S05): 5/hour.
- Per-/24 signup throttle (S35): 20/hour. Catches spray-from-many- IPs-on-the-same-network patterns.
- Honeypot field on the signup form (silently treats success).
- Email verification required (configurable) gates account activation behind a real inbox.
- Captcha integration is deferred (vendor decision pending); the per-/24 throttle is the live primary defense.
A4 — Supply-chain via webhook subscriber
A compromised subscriber URL receives webhook deliveries and uses the captured payloads to escalate (phishing, replay).
Mitigations:
- HMAC-SHA256 signing on every delivery; subscribers can verify authenticity. Per-webhook secret stored AEAD-encrypted at rest.
- Idempotency key on each delivery so replays are detectable.
- SSRF defense rejects subscriber URLs that resolve to private IPs
(operator can opt in for self-hosted CI via
AllowedHosts). - Auto-disable on persistent failure (50 consecutive). Damage is bounded.
A5 — Insider with admin access
A site admin abuses privileges (looking at user data, mass-deleting repos, impersonating users to act on their behalf).
Mitigations:
- Impersonation defaults read-only (
policy.Can+DenyImpersonationReadOnly); writes require a typed-name confirm. - Visible red sticky banner on every page during impersonation.
- Audit row carries BOTH the real admin id and the impersonated id
(
meta.impersonated_user_id) for forensics. - Bootstrap-admin CLI is the only out-of-band elevation path; all
subsequent grants happen through
/admin/users/{id}and audit. - 404 (not 403) for non-admin
/adminaccess prevents privilege enumeration.
A6 — Resource exhaustion
A determined attacker tries to consume CPU, DB connections, or disk faster than our limits.
Mitigations:
- Body-size caps on auth POSTs (so 10MB password bodies don't burn argon2 cost).
- Repo-create throttle (10/hour/user); content-creation throttles on issues/comments/stars/forks.
- Webhook payload cap (25 MiB); response body cap (32 KiB stored).
- Job queue uses
FOR UPDATE SKIP LOCKEDso contention bounded. - pgx pool max-conns capped per process.
Out of scope (v1)
Documented here so they don't get assumed:
- State-actor adversaries. No claim to defend against an attacker with control over CDN/DNS/CA infrastructure.
- Side-channel attacks on the host. Spectre/Meltdown class defenses are the OS/runtime's responsibility.
- Physical access to servers. Standard ops practice; not in app layer.
- Coordinated disclosure pipeline. Future work.
- Penetration test by external party. Future work.
Review cadence
This document is reviewed at the start of every security-touching sprint (S35, S39 beta hardening) and on any major architecture change (S37 deploy, S44 GraphQL API). Significant updates require a PR with an explicit reviewer note in the description.
View source
| 1 | # Threat model — v1 |
| 2 | |
| 3 | A short, concrete document describing who shithub defends against, |
| 4 | what we protect, and how the controls map to attackers. This is the |
| 5 | v1 baseline; it evolves with the codebase. Per the S35 spec, "two |
| 6 | or three pages, not thirty." Pair with `security-checklist.md` for |
| 7 | the per-control test references. |
| 8 | |
| 9 | ## Assets |
| 10 | |
| 11 | In rough decreasing order of consequence if compromised: |
| 12 | |
| 13 | 1. **User credentials.** Password hashes, TOTP secrets, recovery |
| 14 | codes, PATs, SSH public keys. Direct path to account takeover. |
| 15 | 2. **Repository content.** Source code, issues, PR discussions, |
| 16 | private-repo files. Privacy + IP value; for some users, source |
| 17 | IS the product. |
| 18 | 3. **Webhook secrets + delivery payloads.** A leaked webhook secret |
| 19 | lets an attacker spoof events to subscriber systems. Payloads |
| 20 | often contain user content that hadn't been published. |
| 21 | 4. **Site-admin actions.** Suspending users, deleting repos, |
| 22 | impersonating accounts. Trust violations here cascade. |
| 23 | 5. **Server-side resources.** CPU (argon2 hashing, git ops), disk |
| 24 | (repo storage, response-body cap), DB connections. |
| 25 | |
| 26 | ## Attackers |
| 27 | |
| 28 | ### A1 — Compromised user account |
| 29 | |
| 30 | The attacker authenticates as a real user (phished password, leaked |
| 31 | PAT). They have whatever the user has. |
| 32 | |
| 33 | **Mitigations:** |
| 34 | - Per-account session-epoch invalidation: the user can "log out |
| 35 | everywhere" and burn every session at once. |
| 36 | - TOTP 2FA gate (S06) — when enabled, the password alone doesn't |
| 37 | log in. Recovery codes one-shot. |
| 38 | - Audit log on security-relevant actions (auth, settings, admin) |
| 39 | surfaces the attack to the user + admins. |
| 40 | - Suspended-account gate (`policy.Can`) lets an admin freeze a |
| 41 | hijacked account without nuking the user's data. |
| 42 | - Common-password blocklist + argon2id make password-spraying |
| 43 | impractical. |
| 44 | |
| 45 | ### A2 — Malicious public-repo viewer |
| 46 | |
| 47 | An anonymous or freshly-signed-up user crafts payloads against the |
| 48 | public-repo surface: XSS via issue body, CSRF from a public-org page, |
| 49 | SSRF via a webhook to internal infrastructure. |
| 50 | |
| 51 | **Mitigations:** |
| 52 | - All user content is markdown-rendered through `internal/markdown` |
| 53 | with `bluemonday` UGC policy; ad-hoc `template.HTML(...)` is |
| 54 | lint-blocked outside designated render helpers. |
| 55 | - CSRF protection at the router root (nosurf-derived); state- |
| 56 | changing routes inherit it. PAT-only API and git transports are |
| 57 | explicit exemptions documented in the lint script. |
| 58 | - SSRF defense (`internal/security/ssrf`) on every outbound HTTP: |
| 59 | IP block-list + dial-the-IP transport defeats DNS rebinding. |
| 60 | - Webhook secret-decrypt failure auto-disables the hook so a |
| 61 | compromised key doesn't keep delivering forever. |
| 62 | - CSP, Frame-Options DENY, COOP, CORP cut off the embed/clickjack |
| 63 | surface. |
| 64 | |
| 65 | ### A3 — Abusive signup automation |
| 66 | |
| 67 | Botnets create accounts in bulk to spam issues, host abuse content, |
| 68 | or burn through resources. |
| 69 | |
| 70 | **Mitigations:** |
| 71 | - Per-IP signup throttle (S05): 5/hour. |
| 72 | - Per-/24 signup throttle (S35): 20/hour. Catches spray-from-many- |
| 73 | IPs-on-the-same-network patterns. |
| 74 | - Honeypot field on the signup form (silently treats success). |
| 75 | - Email verification required (configurable) gates account |
| 76 | activation behind a real inbox. |
| 77 | - Captcha integration is deferred (vendor decision pending); the |
| 78 | per-/24 throttle is the live primary defense. |
| 79 | |
| 80 | ### A4 — Supply-chain via webhook subscriber |
| 81 | |
| 82 | A compromised subscriber URL receives webhook deliveries and uses |
| 83 | the captured payloads to escalate (phishing, replay). |
| 84 | |
| 85 | **Mitigations:** |
| 86 | - HMAC-SHA256 signing on every delivery; subscribers can verify |
| 87 | authenticity. Per-webhook secret stored AEAD-encrypted at rest. |
| 88 | - Idempotency key on each delivery so replays are detectable. |
| 89 | - SSRF defense rejects subscriber URLs that resolve to private IPs |
| 90 | (operator can opt in for self-hosted CI via `AllowedHosts`). |
| 91 | - Auto-disable on persistent failure (50 consecutive). Damage is |
| 92 | bounded. |
| 93 | |
| 94 | ### A5 — Insider with admin access |
| 95 | |
| 96 | A site admin abuses privileges (looking at user data, mass-deleting |
| 97 | repos, impersonating users to act on their behalf). |
| 98 | |
| 99 | **Mitigations:** |
| 100 | - Impersonation defaults read-only (`policy.Can` + |
| 101 | `DenyImpersonationReadOnly`); writes require a typed-name confirm. |
| 102 | - Visible red sticky banner on every page during impersonation. |
| 103 | - Audit row carries BOTH the real admin id and the impersonated id |
| 104 | (`meta.impersonated_user_id`) for forensics. |
| 105 | - Bootstrap-admin CLI is the only out-of-band elevation path; all |
| 106 | subsequent grants happen through `/admin/users/{id}` and audit. |
| 107 | - 404 (not 403) for non-admin `/admin` access prevents privilege |
| 108 | enumeration. |
| 109 | |
| 110 | ### A6 — Resource exhaustion |
| 111 | |
| 112 | A determined attacker tries to consume CPU, DB connections, or disk |
| 113 | faster than our limits. |
| 114 | |
| 115 | **Mitigations:** |
| 116 | - Body-size caps on auth POSTs (so 10MB password bodies don't burn |
| 117 | argon2 cost). |
| 118 | - Repo-create throttle (10/hour/user); content-creation throttles |
| 119 | on issues/comments/stars/forks. |
| 120 | - Webhook payload cap (25 MiB); response body cap (32 KiB stored). |
| 121 | - Job queue uses `FOR UPDATE SKIP LOCKED` so contention bounded. |
| 122 | - pgx pool max-conns capped per process. |
| 123 | |
| 124 | ## Out of scope (v1) |
| 125 | |
| 126 | Documented here so they don't get assumed: |
| 127 | |
| 128 | - **State-actor adversaries.** No claim to defend against an attacker |
| 129 | with control over CDN/DNS/CA infrastructure. |
| 130 | - **Side-channel attacks on the host.** Spectre/Meltdown class |
| 131 | defenses are the OS/runtime's responsibility. |
| 132 | - **Physical access to servers.** Standard ops practice; not in app |
| 133 | layer. |
| 134 | - **Coordinated disclosure pipeline.** Future work. |
| 135 | - **Penetration test by external party.** Future work. |
| 136 | |
| 137 | ## Review cadence |
| 138 | |
| 139 | This document is reviewed at the start of every security-touching |
| 140 | sprint (S35, S39 beta hardening) and on any major architecture |
| 141 | change (S37 deploy, S44 GraphQL API). Significant updates require a |
| 142 | PR with an explicit reviewer note in the description. |