Prufy notes
The app · 8 min

Trust and safety

Proving ownership is a security feature, so the interesting question is always how it could be fooled.

Threat mapFilter by the layer that stops it
Forged DNS replycore
A fake UDP packet verifies a domain.
A public resolver over HTTPS has to agree: rule (c).
Stale cachecore
An old cached value verifies, or an old no fails.
Authoritative servers decide. Public lag is only propagating.
Lame delegationcore
Servers that don't answer look like a missing record.
No answer is lookup_error, never not_found.
Claiming a shared endingcore
Verifying github.io claims everyone beneath it.
Public suffixes are refused at the door.
Homographcore
аррle.com passes for apple.com.
A mixed-script warning, and punycode is always shown.
Silent revocationcore
An outage takes away someone's ownership.
Temporary failures change nothing, plus 7 days' grace.
Expired codeapi
An old code still verifies.
A check refuses a code past its expiry, even before the worker runs.
Re-registered domainapi
A former owner keeps rights.
Daily re-check, then at-risk, then lapse.
Reading others' claimsapi
Guessing an id reveals a claim.
The same 404 as something that doesn't exist.
Learning who owns a nameapi
The product reveals another customer.
The warning never says who.
Trusting the one-click redirectapi
Someone fakes an approval.
The redirect only triggers a DNS check.
Abuseapi
Flooding claims or checks.
Rate limits on both.
A leaked sessions tableapi
Whoever reads the database signs in as every visitor.
Only each token's SHA-256 is stored. The same goes for setup links and email confirmations.
A forged request from another siteapi
A hidden form stops or changes your claim using your cookie.
SameSite=Lax: the browser leaves the cookie off cross-site POSTs.
A script reading the cookieapi
Injected JavaScript copies the session token.
HttpOnly: page scripts can't see it.
Minting accountsapi
With no sign-up, one client becomes many accounts to dodge the claim limit.
New sessions are limited per IP, and visiting writes nothing.
Signing a stranger up for emailapi
Someone types another person's address into the alerts card.
An address gets one confirmation link and nothing else until it's clicked.
SSRF through a push endpointapi
A fake subscription makes our server POST to an internal address.
isPushEndpoint only accepts four known push services, over HTTPS.
A forwarded setup linkapi
A link passed along lets a stranger take over the claim.
The link can read one record and ask for a check. It can't get a new code or stop the claim.
A setup link leaking by itselfapi
The URL ends up in a search index or a Referer header.
The page is noindex with referrer no-referrer, and the link expires in 30 days.
A silent takeoverapi
A domain moves to another account and the owner never knows.
A transfer alerts both accounts.
Triggering the workerapi
A stranger keeps calling the cron route.
A secret, compared in constant time. With none set, every call is refused.
Tokens in logsapi
Logs leak values.
Only a prefix is ever logged.
SSRF through Domain Connectdns
Our server is made to fetch an internal address.
HTTPS only, public names and IPs, no redirects, a size limit.
A javascript: URLdns
A hostile setting runs code in the browser.
Provider URLs must be HTTPS.
Token reusedb
A code gets used twice.
A unique constraint across the whole table.
Two ownersdb
A race creates two.
A partial unique index.
Replaying a return URLdb
A one-click return gets used again.
A hashed, single-use state.
Tampering with historydb
Someone edits the audit log.
A trigger refuses updates, deletes and truncates.

Decisions worth being able to explain

  • DNS only, no HTML file or meta tag. Those prove control of a web host, which isn't the same as controlling the domain.
  • DNSSEC is recorded, not required. Most domains aren't signed, so requiring it would lock them out, and a broken chain would look like a failed claim.
  • A new proof can take a name. If you can write to the zone, you control the domain. Requiring the old record to be removed would let a former owner block a real sale.

Still open

Open
Whole-script homographs
A name that's entirely Cyrillic gets no warning.
Open
DNS rebinding
A Domain Connect host could answer differently at fetch time. An egress proxy would close this.
Open
A session is one browser
No sign-in on a second device, no sign-out, and clearing cookies loses access from that browser.
Open
Alerts can be lost
They're sent once. If Resend or a push service is down at that moment, that alert is gone.
Open
In-memory rate limits
On Vercel each function instance counts separately, so the limits are looser than they look. That now includes the only brake on new accounts and on invite emails.
Open
Invites reach an unconfirmed inbox
That's what an invite is. It's limited to 5 an hour per account, with a 280-character note rendered as text.
Open
A setup link can't be cancelled
It works until it expires. Nothing revokes one early.
Open
Confirming an email is a GET
A mail scanner that opens links could confirm an address before a person does.
Open
Immediate takeovers
Someone with DNS access takes a name at once. Production likely wants a cooling-off period.