oficina / memoria Public

🤖 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
Paul Campbell pushed by paul@keymail.dev 615d07e23ccb19f808200a0e3c4eeeba7d5f625e parent 44230d6
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