SSRF: unauthenticated AP inbox fetches attacker-controlled actor URLs with no private-IP guard #81
Labels
No labels
bug
duplicate
enhancement
future
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
rbrooks/TeaLeaves#81
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Severity: Medium · Confidence: High · Effort: S · Category: security
Evidence
api/src/lib/apCrypto.ts:51-106-fetchAndCacheActor→fetch(actorUrl)with no host/IP restriction, follows redirects.api/src/lib/apCrypto.ts:182-183, 246-247- called fromverifyInboxSignature/verifyGetSignature.api/src/routes/activitypub.ts:347-390- public unauthenticatedPOST /ap/actors/:apActorId/inbox;244-251-GET /ap/projects/:id.Problem
The AP inbox is unauthenticated by design (gated only by
activitypub.enabled+ block list). To verify a signature the server fetches the actor URL taken from the attacker-suppliedkeyId/actor, with no allowlist, no private-IP/loopback/link-local filtering, and following redirects.Impact
Unauthenticated blind SSRF into the homelab / Tailscale network (port probing, hitting internal GET endpoints). Blind (bodies not reflected). Redirect-following can bounce even a federated host to an internal address.
Fix
Add a shared guard that resolves the target host and rejects RFC1918/loopback/link-local/ULA (and disables or re-validates redirects); apply it to every server-side fetch of a remote-controlled URL (inbound actor fetch + outbound delivery/read).
Acceptance criteria
Related: the actor?signer binding fix (F-03) on the same inbox trust boundary.
Filed from the 2026-07-15 codebase audit. Full report:
docs/.internal/report-2026-07-15.md(gitignored).Fixed in
ab0e89e— v7.1.0 wave 2.New
api/src/lib/ssrfGuard.ts:isBlockedIp()— rejects loopback,10/8,172.16/12,192.168/16,169.254/16(incl. cloud metadata),100.64/10(CGNAT/Tailscale),0/8, multicast/reserved, and IPv6::1/::/fe80::/10/fc00::/7/ IPv4-mapped.assertUrlAllowed()— enforces http(s), resolves the host (all A/AAAA records) and rejects if any resolves to a blocked range. ThrowsSsrfErrorbefore the request is made.safeApFetch()— setsredirect: 'manual'and re-validates every hop (max 4), so a 3xxLocationcan't bounce us to an internal address.Applied to both remote-controlled fetches in
apCrypto.ts:fetchAndCacheActor(inbound actor fetch) anddeliverActivity(outbound delivery).One deliberate design note: an origin the admin has explicitly federated with (enabled row in
federated_instances) bypasses the private-range denylist. Without this, two homelab instances federating over a private/Tailscale address would break — and it mirrors the allowlist approach already used for the #62 outbound path. Unauthenticated attackers hitting the public inbox are not in that table, sokeyId=http://169.254.169.254/…is still rejected.Acceptance criteria:
api/src/test/unit/ssrfGuard.test.ts(plus 172.16/12, 192.168/16, CGNAT, IPv6 ULA/link-local, and public-address negative cases).CI green. Related binding fix: #82.