Security audit fixes: SSRF hardening, replay protection, resource limits
Addresses every finding from the full-codebase security audit:
- CRITICAL: persistent SSRF via ActivityPub actor/inbox URLs. New
internal/activitypub/ssrf.go validates every externally-supplied URL
(https only, rejects loopback/private/link-local/metadata addresses)
and re-validates the resolved IP at dial time to close the DNS-rebinding
gap a resolve-then-connect check would leave open. Wired into
FetchActor, NewHandler's client, and DeliverNewActivity's per-follower
delivery loop.
- CRITICAL: state.db (holds every series' ActivityPub private key in
plaintext) was world-readable (644) on the live deployment. Open() now
hard-fails if it can't chmod the file to 600.
- HIGH: no replay protection on inbound HTTP Signatures. VerifyRequest
now rejects requests whose Date header is more than 5 minutes stale.
- MEDIUM: unbounded upstream response bodies embedded whole into error
messages/logs (Forgejo, AT Proto, ActivityPub clients) — capped at
4 MiB/1 MiB read limits with logged bodies truncated to 500 bytes.
- MEDIUM: subprocess calls (git, rad) had no timeout, so one hung command
could wedge the whole serial sync loop — bounded to 5 minutes via
context.WithTimeout/exec.CommandContext.
- MEDIUM: Authorized Integrations detection silently disables graft's own
push forever once triggered, with no bound — now falls back to pushing
directly after 10 consecutive skipped passes.
Also fixes a dashboard accuracy issue: a mirror side with zero
graft-logged events read as "nothing synced" even when it plainly held
the content — the Forgejo side changes originate on (graft only logs the
pushes it makes, never the pushes it reads from), or a side that received
content via a path graft doesn't log itself (Radicle's own peer-to-peer
gossip, or a Forgejo-to-Forgejo Authorized Integration). Those sides now
show a distinct "source" or "replicated" badge instead, with a legend
entry for each; "nothing synced" is reserved for a series with truly zero
activity anywhere. Also: renamed the dashboard subtitle to "Forgejos &
Radicles syncs", dropped the redundant "Activity by repo" heading, moved
the "last 3 days" window label to the Recent Activity header, and renamed
"Social bridges" to "Social IDs" throughout.
Security audit fixes: SSRF hardening, replay protection, resource limits
Addresses every finding from the full-codebase security audit:
- CRITICAL: persistent SSRF via ActivityPub actor/inbox URLs. New
internal/activitypub/ssrf.go validates every externally-supplied URL
(https only, rejects loopback/private/link-local/metadata addresses)
and re-validates the resolved IP at dial time to close the DNS-rebinding
gap a resolve-then-connect check would leave open. Wired into
FetchActor, NewHandler's client, and DeliverNewActivity's per-follower
delivery loop.
- CRITICAL: state.db (holds every series' ActivityPub private key in
plaintext) was world-readable (644) on the live deployment. Open() now
hard-fails if it can't chmod the file to 600.
- HIGH: no replay protection on inbound HTTP Signatures. VerifyRequest
now rejects requests whose Date header is more than 5 minutes stale.
- MEDIUM: unbounded upstream response bodies embedded whole into error
messages/logs (Forgejo, AT Proto, ActivityPub clients) — capped at
4 MiB/1 MiB read limits with logged bodies truncated to 500 bytes.
- MEDIUM: subprocess calls (git, rad) had no timeout, so one hung command
could wedge the whole serial sync loop — bounded to 5 minutes via
context.WithTimeout/exec.CommandContext.
- MEDIUM: Authorized Integrations detection silently disables graft's own
push forever once triggered, with no bound — now falls back to pushing
directly after 10 consecutive skipped passes.
Also fixes a dashboard accuracy issue: a mirror side with zero
graft-logged events read as "nothing synced" even when it plainly held
the content — the Forgejo side changes originate on (graft only logs the
pushes it makes, never the pushes it reads from), or a side that received
content via a path graft doesn't log itself (Radicle's own peer-to-peer
gossip, or a Forgejo-to-Forgejo Authorized Integration). Those sides now
show a distinct "source" or "replicated" badge instead, with a legend
entry for each; "nothing synced" is reserved for a series with truly zero
activity anywhere. Also: renamed the dashboard subtitle to "Forgejos &
Radicles syncs", dropped the redundant "Activity by repo" heading, moved
the "last 3 days" window label to the Recent Activity header, and renamed
"Social bridges" to "Social IDs" throughout.
Security audit fixes: SSRF hardening, replay protection, resource limits
Addresses every finding from the full-codebase security audit:
- CRITICAL: persistent SSRF via ActivityPub actor/inbox URLs. New
internal/activitypub/ssrf.go validates every externally-supplied URL
(https only, rejects loopback/private/link-local/metadata addresses)
and re-validates the resolved IP at dial time to close the DNS-rebinding
gap a resolve-then-connect check would leave open. Wired into
FetchActor, NewHandler's client, and DeliverNewActivity's per-follower
delivery loop.
- CRITICAL: state.db (holds every series' ActivityPub private key in
plaintext) was world-readable (644) on the live deployment. Open() now
hard-fails if it can't chmod the file to 600.
- HIGH: no replay protection on inbound HTTP Signatures. VerifyRequest
now rejects requests whose Date header is more than 5 minutes stale.
- MEDIUM: unbounded upstream response bodies embedded whole into error
messages/logs (Forgejo, AT Proto, ActivityPub clients) — capped at
4 MiB/1 MiB read limits with logged bodies truncated to 500 bytes.
- MEDIUM: subprocess calls (git, rad) had no timeout, so one hung command
could wedge the whole serial sync loop — bounded to 5 minutes via
context.WithTimeout/exec.CommandContext.
- MEDIUM: Authorized Integrations detection silently disables graft's own
push forever once triggered, with no bound — now falls back to pushing
directly after 10 consecutive skipped passes.
Also fixes a dashboard accuracy issue: a mirror side with zero
graft-logged events read as "nothing synced" even when it plainly held
the content — the Forgejo side changes originate on (graft only logs the
pushes it makes, never the pushes it reads from), or a side that received
content via a path graft doesn't log itself (Radicle's own peer-to-peer
gossip, or a Forgejo-to-Forgejo Authorized Integration). Those sides now
show a distinct "source" or "replicated" badge instead, with a legend
entry for each; "nothing synced" is reserved for a series with truly zero
activity anywhere. Also: renamed the dashboard subtitle to "Forgejos &
Radicles syncs", dropped the redundant "Activity by repo" heading, moved
the "last 3 days" window label to the Recent Activity header, and renamed
"Social bridges" to "Social IDs" throughout.
v2 M2: inbox, follows, and signed delivery to followers
- httpsig.go: draft-cavage HTTP Signatures (sign outbound, verify inbound)
— the fiddliest part of ActivityPub federation, so it ships with a
round-trip test (sign -> receive -> verify) plus a tamper-detection
test, rather than trusting it by inspection.
- Inbox accepts Follow (verifies the signature against the follower's own
published key, records them, replies with a signed Accept) and
Undo{Follow} (removes them). Anything else (replies included) is
accepted and ignored until the comment bridge, M3.
- After each sync pass, DeliverNewActivity signs and POSTs a Create{Note}
to every follower's inbox for whatever landed in activity_log since
that series' last delivery — tracked per-series so a restart never
redelivers history.
- /social now shows each series' live follower count.
Verified live against real malformed/unreachable-actor requests (400 on
bad JSON, 502 on an actor that fails to resolve) with no crash — a real
follow from an actual fediverse account is still the outstanding
end-to-end check, not yet done.
Security audit fixes: SSRF hardening, replay protection, resource limits
Addresses every finding from the full-codebase security audit:
- CRITICAL: persistent SSRF via ActivityPub actor/inbox URLs. New
internal/activitypub/ssrf.go validates every externally-supplied URL
(https only, rejects loopback/private/link-local/metadata addresses)
and re-validates the resolved IP at dial time to close the DNS-rebinding
gap a resolve-then-connect check would leave open. Wired into
FetchActor, NewHandler's client, and DeliverNewActivity's per-follower
delivery loop.
- CRITICAL: state.db (holds every series' ActivityPub private key in
plaintext) was world-readable (644) on the live deployment. Open() now
hard-fails if it can't chmod the file to 600.
- HIGH: no replay protection on inbound HTTP Signatures. VerifyRequest
now rejects requests whose Date header is more than 5 minutes stale.
- MEDIUM: unbounded upstream response bodies embedded whole into error
messages/logs (Forgejo, AT Proto, ActivityPub clients) — capped at
4 MiB/1 MiB read limits with logged bodies truncated to 500 bytes.
- MEDIUM: subprocess calls (git, rad) had no timeout, so one hung command
could wedge the whole serial sync loop — bounded to 5 minutes via
context.WithTimeout/exec.CommandContext.
- MEDIUM: Authorized Integrations detection silently disables graft's own
push forever once triggered, with no bound — now falls back to pushing
directly after 10 consecutive skipped passes.
Also fixes a dashboard accuracy issue: a mirror side with zero
graft-logged events read as "nothing synced" even when it plainly held
the content — the Forgejo side changes originate on (graft only logs the
pushes it makes, never the pushes it reads from), or a side that received
content via a path graft doesn't log itself (Radicle's own peer-to-peer
gossip, or a Forgejo-to-Forgejo Authorized Integration). Those sides now
show a distinct "source" or "replicated" badge instead, with a legend
entry for each; "nothing synced" is reserved for a series with truly zero
activity anywhere. Also: renamed the dashboard subtitle to "Forgejos &
Radicles syncs", dropped the redundant "Activity by repo" heading, moved
the "last 3 days" window label to the Recent Activity header, and renamed
"Social bridges" to "Social IDs" throughout.
Security audit fixes: SSRF hardening, replay protection, resource limits
Addresses every finding from the full-codebase security audit:
- CRITICAL: persistent SSRF via ActivityPub actor/inbox URLs. New
internal/activitypub/ssrf.go validates every externally-supplied URL
(https only, rejects loopback/private/link-local/metadata addresses)
and re-validates the resolved IP at dial time to close the DNS-rebinding
gap a resolve-then-connect check would leave open. Wired into
FetchActor, NewHandler's client, and DeliverNewActivity's per-follower
delivery loop.
- CRITICAL: state.db (holds every series' ActivityPub private key in
plaintext) was world-readable (644) on the live deployment. Open() now
hard-fails if it can't chmod the file to 600.
- HIGH: no replay protection on inbound HTTP Signatures. VerifyRequest
now rejects requests whose Date header is more than 5 minutes stale.
- MEDIUM: unbounded upstream response bodies embedded whole into error
messages/logs (Forgejo, AT Proto, ActivityPub clients) — capped at
4 MiB/1 MiB read limits with logged bodies truncated to 500 bytes.
- MEDIUM: subprocess calls (git, rad) had no timeout, so one hung command
could wedge the whole serial sync loop — bounded to 5 minutes via
context.WithTimeout/exec.CommandContext.
- MEDIUM: Authorized Integrations detection silently disables graft's own
push forever once triggered, with no bound — now falls back to pushing
directly after 10 consecutive skipped passes.
Also fixes a dashboard accuracy issue: a mirror side with zero
graft-logged events read as "nothing synced" even when it plainly held
the content — the Forgejo side changes originate on (graft only logs the
pushes it makes, never the pushes it reads from), or a side that received
content via a path graft doesn't log itself (Radicle's own peer-to-peer
gossip, or a Forgejo-to-Forgejo Authorized Integration). Those sides now
show a distinct "source" or "replicated" badge instead, with a legend
entry for each; "nothing synced" is reserved for a series with truly zero
activity anywhere. Also: renamed the dashboard subtitle to "Forgejos &
Radicles syncs", dropped the redundant "Activity by repo" heading, moved
the "last 3 days" window label to the Recent Activity header, and renamed
"Social bridges" to "Social IDs" throughout.
Security audit fixes: SSRF hardening, replay protection, resource limits
Addresses every finding from the full-codebase security audit:
- CRITICAL: persistent SSRF via ActivityPub actor/inbox URLs. New
internal/activitypub/ssrf.go validates every externally-supplied URL
(https only, rejects loopback/private/link-local/metadata addresses)
and re-validates the resolved IP at dial time to close the DNS-rebinding
gap a resolve-then-connect check would leave open. Wired into
FetchActor, NewHandler's client, and DeliverNewActivity's per-follower
delivery loop.
- CRITICAL: state.db (holds every series' ActivityPub private key in
plaintext) was world-readable (644) on the live deployment. Open() now
hard-fails if it can't chmod the file to 600.
- HIGH: no replay protection on inbound HTTP Signatures. VerifyRequest
now rejects requests whose Date header is more than 5 minutes stale.
- MEDIUM: unbounded upstream response bodies embedded whole into error
messages/logs (Forgejo, AT Proto, ActivityPub clients) — capped at
4 MiB/1 MiB read limits with logged bodies truncated to 500 bytes.
- MEDIUM: subprocess calls (git, rad) had no timeout, so one hung command
could wedge the whole serial sync loop — bounded to 5 minutes via
context.WithTimeout/exec.CommandContext.
- MEDIUM: Authorized Integrations detection silently disables graft's own
push forever once triggered, with no bound — now falls back to pushing
directly after 10 consecutive skipped passes.
Also fixes a dashboard accuracy issue: a mirror side with zero
graft-logged events read as "nothing synced" even when it plainly held
the content — the Forgejo side changes originate on (graft only logs the
pushes it makes, never the pushes it reads from), or a side that received
content via a path graft doesn't log itself (Radicle's own peer-to-peer
gossip, or a Forgejo-to-Forgejo Authorized Integration). Those sides now
show a distinct "source" or "replicated" badge instead, with a legend
entry for each; "nothing synced" is reserved for a series with truly zero
activity anywhere. Also: renamed the dashboard subtitle to "Forgejos &
Radicles syncs", dropped the redundant "Activity by repo" heading, moved
the "last 3 days" window label to the Recent Activity header, and renamed
"Social bridges" to "Social IDs" throughout.