diff --git a/docs/design-sketch-proposal.md b/docs/design-sketch-proposal.md index df3210f..8c9e333 100644 --- a/docs/design-sketch-proposal.md +++ b/docs/design-sketch-proposal.md @@ -476,8 +476,27 @@ The `events/subscribe` response MAY include a `deliveryStatus` object when refre } ``` +```jsonc +// Subscription being actively rate-limited (deliveries delayed, not failing) +{ + "id": "sub_a3f1c8e2b0d49f7e", + "refreshBefore": "2026-02-19T17:00:00Z", + "cursor": "cursor_xyz", + "truncated": false, + "deliveryStatus": { + "active": true, + "lastDeliveryAt": "2026-02-19T16:28:00Z", + "lastError": null, + "throttled": true, + "retryAfterMs": 60000 + } +} +``` + `deliveryStatus` is OPTIONAL — servers MAY omit it entirely. When present, `active` indicates whether the server is currently delivering events (`false` means it has suspended retries after repeated failures; the refresh that returned this status has just reactivated it). `lastError` MUST be a server-generated category string — one of `connection_refused`, `timeout`, `tls_error`, `http_4xx`, `http_5xx`, `challenge_failed` — and MUST NOT include raw response bodies, headers, or status lines from the endpoint, to avoid serving as a response oracle for attacker-chosen URLs. The client can use this information to diagnose connectivity or authentication issues with the webhook endpoint. The same categories also appear as `data.reason` on a `-32015 CallbackEndpointError` returned synchronously from `events/subscribe`. +`throttled` (optional boolean) distinguishes active rate-limiting from failure-driven suspension: `true` means the server is currently limiting outbound deliveries for this subscription — deliveries are being delayed or coalesced, not failing — so the client should not read reduced traffic as "nothing is happening" or as an endpoint problem. `retryAfterMs` (optional integer) accompanies it as a hint for how long the client should expect throttling to last before normal delivery resumes. Both fields are advisory; servers that do not rate-limit outbound deliveries never send them, and a `gap` envelope still covers the case where throttling caused events to be skipped outright. + #### Webhook Security **SSRF prevention.** The server MUST validate callback URLs. Servers SHOULD reject URLs whose resolved IP is not globally routable per the [IANA IPv4](https://www.iana.org/assignments/iana-ipv4-special-registry/) and [IPv6](https://www.iana.org/assignments/iana-ipv6-special-registry/) Special-Purpose Address Registries (illustrative examples: `127.0.0.0/8`, `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `169.254.0.0/16`, `::1`, `fc00::/7`, `fe80::/10`) unless explicitly configured to allow them. To prevent DNS rebinding, this validation MUST be performed at delivery time, not only at subscribe time: the server resolves the hostname, checks the resolved IP against the blocklist, and connects directly to that validated IP (sending the original hostname in the `Host` header / TLS SNI) so the address cannot change between check and connect. Subscribe-time validation alone is insufficient — a rebinding attacker returns a public IP at subscribe time and a private IP at delivery time. Webhook delivery requests MUST NOT follow HTTP redirects, since a redirect can target an internal address that bypasses the blocklist. Servers MAY additionally maintain an allowlist of permitted callback URL patterns.