oficina / memoria Public

Clone
git clone https://amadan.net/oficina/memoria

Plain git — no account needed to clone.

Download

Download this file

1# Draft security report — not filed
2
3**For Paul to send, through whichever channel he judges right.** I have
4not filed this anywhere. `github.com/carlosframework/rastrillo` is a
5public repo and this is an unauthenticated server-side request forgery
6affecting every app built on it, so where and how it gets disclosed is a
7decision for the person who owns the org, not for an agent at 3am.
8
9Found 2026-09-05 while building memoria's sign-in dial guard (gate 33).
10memoria's §3.2 has recorded the shallow version of this since its code
11review — *"an anonymous visitor can make the instance dial any domain they
12type"* — and Fable's review of QUESTIONS #6 said the fetcher's hardening
13should be checked before assuming it was only an existence leak. It was
14checked. It is not only an existence leak.
15
16---
17
18## The class
19
20**Unauthenticated SSRF with an attacker-chosen destination**, reachable
21from the sign-in form of any rastrillo app, before any admission check.
22
23## Where
24
25`github.com/keymaildev/signin`, `classify.go` — the `Classifier`, called
26from `rastrillo/auth`'s `Begin` handler (`auth/handlers.go`, the
27`a.flow.Begin(...)` call) which runs **before** `auth.Config.Authorize`.
28
29## Mechanism
30
311. `POST /auth/begin` with any address. No session, no admission check.
322. `Classifier.delegate` resolves TXT for `_keymail.<domain>`.
333. A record of the form `v=… host=…` returns **`host` verbatim**. There is
34 no allowlist, no scheme check, no port restriction and no
35 private-address-range check on that value.
364. `getJSON` then issues a GET to a URL built from it, using
37 `&http.Client{Timeout: 5s}` — **default redirect policy**, so up to ten
38 redirects are followed to wherever they point.
395. On NXDOMAIN, `delegate` returns the bare domain, so the fetch happens
40 for essentially any syntactically valid domain, not only delegated ones.
41
42Anyone who controls DNS for any domain therefore chooses the destination.
43The instance makes the request from its own network position.
44
45## Why it matters more than the existence leak
46
47The recorded consequence was: instance existence, egress IP, timing
48channel. The actual reach is whatever the instance's network can reach —
49link-local metadata endpoints, private ranges, and internal services that
50answer GET — with redirects followed and a 1 MiB body read. No content
51returns to the attacker directly, which bounds it, but response *timing*
52and *status* are observable through the sign-in flow's own behaviour.
53
54## Suggested fix, and the pattern already exists in this codebase
55
56`amadan.net/oficina/suite`'s `transport.go` solves the same problem for
57sibling calls and is the shape to copy:
58
59- the host is checked **before the transport dials**, so a refused host
60 costs no connection;
61- redirects are refused outright rather than bounded — *"an allowed host
62 has to be reached to tell us to redirect"*;
63- the check is exported separately from the client, because more than the
64 HTTP path needs it.
65
66For signin specifically: refuse private, loopback, link-local and
67unspecified ranges after resolution (not before — resolve, then check the
68IP, or the check is bypassable by DNS); pin the scheme to https and the
69port to 443; refuse redirects. And in `rastrillo/auth`, **run
70`Config.Authorize` before `flow.Begin`**, which removes the anonymous
71reachability independently of the fetcher's own hardening.
72
73## What memoria has done, and what it does not fix
74
75memoria's `beginGuard` (this branch, `0bd3023`) runs the admission
76predicate before the classifier, so an unadmitted address triggers no DNS
77query and no fetch. Gate 33 holds it, mutation-tested five ways.
78
79**It does not fix this.** It narrows memoria's exposure to *owners*, and
80it does nothing for any other rastrillo app. An owner can still reach the
81unhardened fetcher. The fix belongs upstream.
82
83## Disclosure note
84
85Written to describe the defect and its fix. It deliberately contains no
86ready-to-run reproduction. Whether it goes to a public issue, a private
87advisory, or straight to a patch is Paul's call — a public issue on a
88public repo publishes the reachability to everyone running a rastrillo app
89before any of them can deploy a fix, which argues for the patch first.
90