markdown · 7996 bytes Raw Blame History

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:

  1. User credentials. Password hashes, TOTP secrets, recovery codes, PATs, SSH public keys. Direct path to account takeover.
  2. Repository content. Source code, issues, PR discussions, private-repo files. Privacy + IP value; for some users, source IS the product.
  3. 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.
  4. Site-admin actions. Suspending users, deleting repos, impersonating accounts. Trust violations here cascade.
  5. 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/markdown with bluemonday UGC policy; ad-hoc template.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 /admin access 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 LOCKED so 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.

S39 hardening review (2026-05-09)

The S39 internal pen-test (3 days, scoped to the OWASP top set + auth + git + webhook SSRF) noted the following considerations for v1 — none introduce a new attacker class, but they sharpen how A1–A6 are addressed:

  • A1 — compromised account. The S38 introduction of the finalized "sign out everywhere" surface (per-account session epoch) is the operator's primary lever. The audit flagged that rotating the session signing key (docs/internal/runbooks/rotate-secrets.md) is also a global kill-switch — useful for "we suspect the cookie database leaked." Documented; no code change.
  • A2 — public viewer. The render.go fix landing in S39 (internal/web/render/render.go) closes a class of silent- blank-page bugs that, while not a vulnerability themselves, made it harder to notice missing authorization gates during development. Fail-loud at parse time is now the rule.
  • A4 — webhook subscriber. The SSRF defense (internal/security/ssrf/) gets re-tested every release; S39 added the audit-a11y and load-test CI scaffolding but did not change the SSRF surface.
  • A6 — resource exhaustion. The k6 scenarios in tests/load/k6/scenarios/ exercise the rate-limit floors. The S39 spec calls out "0% 5xx errors; rate-limit-driven 429s expected and counted" — confirmed in the load-test design.

Out-of-band watchlist (track separately)

These don't fit the A1–A6 attacker model but operators should keep an eye on them:

  • Dependency-supply-chain on the Go side. go.sum pinning is enforced; we don't yet do reproducible-build verification.
  • The docs subdomain serving from Spaces. A bucket policy mistake there could let an attacker stage a phishing page on docs.shithub.sh. Mitigated by Caddy's CSP and the explicit reverse-proxy origin (deploy/docs-site/Caddyfile.snippet).
  • PAT prefix recognition by external secret scanners. shp_ is documented in docs/public/user/personal-access- tokens.md and recognised by GitGuardian/GitHub's scanners; if we ever rotate the prefix, coordinate with them so leaked tokens still get caught upstream.
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.
143
144 ## S39 hardening review (2026-05-09)
145
146 The S39 internal pen-test (3 days, scoped to the OWASP top set +
147 auth + git + webhook SSRF) noted the following considerations
148 for v1 — none introduce a new attacker class, but they sharpen
149 how A1–A6 are addressed:
150
151 - **A1 — compromised account.** The S38 introduction of the
152 finalized "sign out everywhere" surface (per-account session
153 epoch) is the operator's primary lever. The audit flagged that
154 rotating the session signing key
155 (`docs/internal/runbooks/rotate-secrets.md`) is also a global
156 kill-switch — useful for "we suspect the cookie database
157 leaked." Documented; no code change.
158 - **A2 — public viewer.** The render.go fix landing in S39
159 (`internal/web/render/render.go`) closes a class of silent-
160 blank-page bugs that, while not a vulnerability themselves,
161 made it harder to notice missing authorization gates during
162 development. Fail-loud at parse time is now the rule.
163 - **A4 — webhook subscriber.** The SSRF defense
164 (`internal/security/ssrf/`) gets re-tested every release; S39
165 added the `audit-a11y` and `load-test` CI scaffolding but did
166 not change the SSRF surface.
167 - **A6 — resource exhaustion.** The k6 scenarios in
168 `tests/load/k6/scenarios/` exercise the rate-limit floors. The
169 S39 spec calls out "0% 5xx errors; rate-limit-driven 429s
170 expected and counted" — confirmed in the load-test design.
171
172 ## Out-of-band watchlist (track separately)
173
174 These don't fit the A1–A6 attacker model but operators should
175 keep an eye on them:
176
177 - **Dependency-supply-chain on the Go side.** `go.sum` pinning
178 is enforced; we don't yet do reproducible-build verification.
179 - **The docs subdomain serving from Spaces.** A bucket
180 policy mistake there could let an attacker stage a phishing
181 page on `docs.shithub.sh`. Mitigated by Caddy's CSP
182 and the explicit reverse-proxy origin
183 (`deploy/docs-site/Caddyfile.snippet`).
184 - **PAT prefix recognition by external secret scanners.**
185 `shp_` is documented in `docs/public/user/personal-access-
186 tokens.md` and recognised by GitGuardian/GitHub's scanners;
187 if we ever rotate the prefix, coordinate with them so leaked
188 tokens still get caught upstream.