oficina / memoria Public

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

Plain git — no account needed to clone.

Download

Download this file

Draft security report — not filed

For Paul to send, through whichever channel he judges right. I have not filed this anywhere. github.com/carlosframework/rastrillo is a public repo and this is an unauthenticated server-side request forgery affecting every app built on it, so where and how it gets disclosed is a decision for the person who owns the org, not for an agent at 3am.

Found 2026-09-05 while building memoria's sign-in dial guard (gate 33). memoria's §3.2 has recorded the shallow version of this since its code review — "an anonymous visitor can make the instance dial any domain they type" — and Fable's review of QUESTIONS #6 said the fetcher's hardening should be checked before assuming it was only an existence leak. It was checked. It is not only an existence leak.

---

The class

Unauthenticated SSRF with an attacker-chosen destination, reachable from the sign-in form of any rastrillo app, before any admission check.

Where

github.com/keymaildev/signin, classify.go — the Classifier, called from rastrillo/auth's Begin handler (auth/handlers.go, the a.flow.Begin(...) call) which runs before auth.Config.Authorize.

Mechanism

  1. POST /auth/begin with any address. No session, no admission check.
  2. Classifier.delegate resolves TXT for _keymail.<domain>.
  3. A record of the form v=… host=… returns host verbatim. There is no allowlist, no scheme check, no port restriction and no private-address-range check on that value.
  4. getJSON then issues a GET to a URL built from it, using &http.Client{Timeout: 5s} — default redirect policy, so up to ten redirects are followed to wherever they point.
  5. On NXDOMAIN, delegate returns the bare domain, so the fetch happens for essentially any syntactically valid domain, not only delegated ones.

Anyone who controls DNS for any domain therefore chooses the destination. The instance makes the request from its own network position.

Why it matters more than the existence leak

The recorded consequence was: instance existence, egress IP, timing channel. The actual reach is whatever the instance's network can reach — link-local metadata endpoints, private ranges, and internal services that answer GET — with redirects followed and a 1 MiB body read. No content returns to the attacker directly, which bounds it, but response timing and status are observable through the sign-in flow's own behaviour.

Suggested fix, and the pattern already exists in this codebase

amadan.net/oficina/suite's transport.go solves the same problem for sibling calls and is the shape to copy:

  • the host is checked before the transport dials, so a refused host costs no connection;
  • redirects are refused outright rather than bounded — "an allowed host has to be reached to tell us to redirect";
  • the check is exported separately from the client, because more than the HTTP path needs it.

For signin specifically: refuse private, loopback, link-local and unspecified ranges after resolution (not before — resolve, then check the IP, or the check is bypassable by DNS); pin the scheme to https and the port to 443; refuse redirects. And in rastrillo/auth, run Config.Authorize before flow.Begin, which removes the anonymous reachability independently of the fetcher's own hardening.

What memoria has done, and what it does not fix

memoria's beginGuard (this branch, 0bd3023) runs the admission predicate before the classifier, so an unadmitted address triggers no DNS query and no fetch. Gate 33 holds it, mutation-tested five ways.

It does not fix this. It narrows memoria's exposure to owners, and it does nothing for any other rastrillo app. An owner can still reach the unhardened fetcher. The fix belongs upstream.

Disclosure note

Written to describe the defect and its fix. It deliberately contains no ready-to-run reproduction. Whether it goes to a public issue, a private advisory, or straight to a patch is Paul's call — a public issue on a public repo publishes the reachability to everyone running a rastrillo app before any of them can deploy a fix, which argues for the patch first.