🤖 spec: §3.2's sign-in entry understated the dial for three drafts
It stopped at existence, egress IP and a timing channel — which is what an arbitrary DOMAIN buys. The fetcher takes host= from that domain's TXT record verbatim and follows redirects, so the destination is the submitter's to choose: an unauthenticated SSRF reaching whatever this instance's network reaches. Also records the residual gate 27 cannot close, which the gate's own entry already claimed §3.2 recorded: the guard sits in front of signin's per-address rate limiter, so an owner and a stranger diverge after five submissions and enumeration is unthrottled.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H3VKtXnq6fSmM2F11Bjcsy
1 file changed,
+27
−9
docs/superpowers/specs/2026-09-03-memoria-design.md+27 −9
diff --git a/docs/superpowers/specs/2026-09-03-memoria-design.md b/docs/superpowers/specs/2026-09-03-memoria-design.md| index 66df086..9c77e4e 100644 |
| --- a/docs/superpowers/specs/2026-09-03-memoria-design.md |
| +++ b/docs/superpowers/specs/2026-09-03-memoria-design.md |
| @@ -556,15 +556,33 @@ which team is awake. |
| **From a sibling:** bodies transit the router in plaintext on every S3 |
| fetch (§6.1). |
| -**From the sign-in form, to anyone at all.** rastrillo's classifier |
| -fetches `https://<the address's domain>/.well-known/keymail` when a |
| -sign-in begins, and it does so **before** the admission gate — so an |
| -anonymous visitor who types `anyone@attacker.example` makes this instance |
| -dial `attacker.example`. What that yields is the instance's existence, |
| -reachability, egress address and a timing channel; no content. It is |
| -listed because draft 3 previously let the product claim "nothing leaves |
| -this instance", which this makes false. The fix is not memoria's to make |
| -alone (§13). |
| +**From the sign-in form — and this entry understated it for three |
| +drafts.** rastrillo's classifier resolves and fetches the submitted |
| +address's domain when a sign-in begins, **before** the admission gate. The |
| +entry used to stop at "the instance's existence, reachability, egress |
| +address and a timing channel; no content", which is what an *arbitrary |
| +domain* buys. Reading the fetcher on 2026-09-05 showed it buys more: the |
| +`host=` in a domain's `_keymail` TXT record is taken verbatim, with no |
| +allowlist, no scheme or port restriction and no private-range check, and |
| +fetched by a client that follows redirects. **So the destination is the |
| +submitter's to choose, not merely the domain's** — an unauthenticated SSRF |
| +whose reach is whatever this instance's network can reach. |
| + |
| +**memoria's half is closed** (gate 27): the admission predicate now runs |
| +before the classifier, so an unadmitted address produces no DNS query and |
| +no fetch. **Two things that does not fix.** An *owner* still reaches the |
| +unhardened fetcher, and every other rastrillo app is untouched — the real |
| +fix is upstream (§13, and `SECURITY-DRAFT-rastrillo-signin.md`). And the |
| +guard sits in front of signin's per-address rate limiter, so **an owner |
| +and a stranger become distinguishable after five submissions** — the |
| +owner's sixth answers `?err=rate`, the stranger's answers `?sent=1` |
| +forever, and refused addresses consume no per-IP budget, so enumerating |
| +the owners list is unthrottled. That is a smaller leak than the dial and |
| +it is a real one; the budget is inside signin and cannot be reached from |
| +here. |
| + |
| +This is listed because draft 3 let the product claim "nothing leaves this |
| +instance", which it makes false. |
| **Not a leak, corrected:** a document's `external_id` was an unkeyed |
| SHA-256 of its body, which let anyone holding the bucket and a candidate |