You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
* fix(aggregator): wrap unexpected XRPC errors and harden PDS egress against SSRF
Finding 17 (Sol): a non-XRPC failure resolving the labeler policy (e.g. a D1
error) escaped `handleXrpc` unwrapped, hitting workerd's bare 500 without the
CORS or `private, no-store` headers every aggregator response carries. Convert
unexpected errors to a generic 500 through the same wrapper, logging the
internal detail but never leaking it to the client. XRPCError handling is
unchanged.
SSRF follow-up: `pds-verify.ts` fetched the publisher-controlled PDS endpoint
(from a DID document) following redirects with no scheme/DNS/reserved-address
validation and no body-read deadline. Route it through the shared hardened
egress (`resolveAndValidateExternalUrl` + injected `cloudflareDohResolver`):
HTTPS-only, per-hop re-resolution under `redirect: "manual"` with a bounded
redirect count, a wall-clock deadline spanning redirects and the body read, and
fail-closed when no resolver is injected. Blocks are reported as a new permanent
`PDS_ADDRESS_BLOCKED` reason; existing 404/5xx/too-large/invalid-proof and the
transient network classification are preserved.
* fix(aggregator): retry PDS verification on resolver-infra failures, don't dead-letter
`assertFetchableUrl` collapsed every `SsrfError` from
`resolveAndValidateExternalUrl` into the permanent `PDS_ADDRESS_BLOCKED`, but
that helper also throws `SsrfError` when the injected DNS resolver itself fails
(DoH network error / SERVFAIL / timeout) — transient infrastructure, not a
disallowed publisher address. A DoH blip therefore permanently dead-lettered
legitimate records and risked mass dead-lettering during a resolver outage.
`SsrfError.code` is the same constant for both cases, so key on the message
prefix the helper uses only for the resolver-threw path (`Could not resolve
hostname:`): map that to the transient `PDS_NETWORK_ERROR` (retried), and keep
genuine address blocks — private/reserved IP, blocked host, scheme — as the
permanent `PDS_ADDRESS_BLOCKED`. A test asserts the transient mapping so a future
wording change in the shared core helper breaks loudly rather than silently
mis-classifying. Also adds a redirect-hop-limit boundary test.
* fix(aggregator): retry PDS verification on empty DNS answer, don't dead-letter
An empty resolver answer (NXDOMAIN / NOERROR-NODATA / CNAME-only) makes
`resolveAndValidateExternalUrl` throw `SsrfError("Hostname resolved to no
addresses")`, which the classifier mapped to the permanent PDS_ADDRESS_BLOCKED
— so a host mid-DNS-propagation lost the record to a dead-letter. Treat the
empty-answer message as a transient resolution failure (PDS_NETWORK_ERROR,
retried) alongside the existing resolver-threw case; a genuinely-gone host
still dead-letters via retry exhaustion. Genuine private/reserved/blocked-host
addresses stay permanent. Keyed on the shared helper's message as a module
constant with a test, matching the resolver-failure-prefix approach.
* fix(aggregator): delay re-delivery on transient DNS-resolution failure
The records queue re-delivers immediately (no `retry_delay`), so classifying an
empty DNS answer / resolver outage as transient only helped if the retry waited
for propagation — otherwise all `max_retries` (5) burn in seconds and the record
DLQs as UNEXPECTED_ERROR before DNS settles. `PdsVerificationError` now carries
an optional `retryAfterSeconds`, set (to 60s) only on the transient DNS-resolution
path; the consumer passes it as `retry({ delaySeconds })`. Other transient
retries (network, 5xx, timeout) stay immediate.
Delay strategy: fixed 60s. With max_retries: 5 that spreads retries across a
~5-minute propagation window. Chosen for reconciliation with labeler #2113.
0 commit comments