v0.40.6 — Patch: Outbound-fetch hardening and latent plugin watchdogs
If you operate webhooks or notification channels, read this first. This patch closes the DNS-rebinding window in outbound URL validation and extends validation to three previously-unchecked paths. Operator-visible changes: (1) hostnames that return no DNS answer are now rejected — previously they passed as valid, so a webhook target that stops resolving will now fail validation (400 at configuration time, a recorded failed delivery at dispatch time); (2) the notification webhook channel now rejects internal/LAN targets at delivery time — operators pointing it at a deliberately internal destination (self-hosted ntfy, a local bridge) must list the hostname in ORCYSSRFALLOWLIST; (3) remote webhook endpoints whose hostnames resolve to private space now 400 at create/PATCH — existing stored endpoints keep listing and enabling, and simply fail per dispatch with a recorded failed-delivery row.
Outbound fetches are pinned to their validated resolution
The canonical URL checker resolved DNS, returned a verdict, and every caller then fetched by hostname — resolving DNS a second time. A rebinding hostname (public answer at validation, private at fetch) bypassed the private-IP block; worse, a hostname that returned no DNS answer passed as valid. The checker now fails closed on empty answer sets, returns its resolved addresses, and a new shared helper validates and fetches pinned to exactly those addresses via a custom connection lookup — one DNS resolution total, TLS/SNI still bound to the hostname (verified down to the connector), fail-closed redirects, and a ten-second timeout that now also bounds response-body reads. The plugin webhook capability and automation call_webhook actions use the helper, and a previously committed test that pinned the fail-open behavior is rewritten to the closed contract.
Three previously-unvalidated paths now use the canonical checker
Remote webhook registration validated URLs with a hostname-prefix list (no resolution, no IPv4-mapped handling); the compact remote webhook dispatcher fetched stored endpoints with no dispatch-time check at all; and the notification webhook channel had no validation anywhere. All three now validate through the canonical checker — remote webhooks at configuration and dispatch time, the notification channel at delivery time (it has no configuration surface) — pinned and fail-closed like the rest. SECURITY.md's coverage statement names exactly which paths pin.
Two latent plugin watchdogs
Notification channels and post-interceptors kept the zero timeout default that disables the invocation watchdog — the same never-settling hang class fixed for automation actions in v0.40.4. Both now default to 30 seconds. This is latent today (channel delivery is not yet wired into production notification dispatch, and post-interceptors are fire-and-forget), and quarantine accounting is unchanged per the invocation policy: the watchdog terminates and faults the run; faults still do not count toward quarantine for these kinds. A manifest-declared timeoutMs: 0 remains an explicit opt-out for both.
Verification, including an adversarial review
Before release, an independent three-verifier review pass attacked the changes. It confirmed SNI/certificate validation cannot be silently re-targeted under pinned lookup and that the connection-pool eviction cannot break in-flight fetches; its findings are all addressed here or tracked: a dead statement and an over-broad doc claim were fixed in-tree, and two follow-ups are filed (four remaining validate-then-unpinned-fetch surfaces — outgoing-webhook delivery, Slack, Discord, chat — and the Jira adapter's member-supplied base URL, which ships unvalidated and is the highest-priority item in the backlog).
@orcy/api typecheck and build: clean. @orcy/mcp suite: 38 files, 706 passed. corepack pnpm lint: 0 errors.@orcy/api suite: 414 files, 6,535 passed / 2 skipped / 0 failed.- Fix tests were authored first and proven discriminating via mutate-and-revert; the rebind pinning, empty-DNS fail-closed, literal-IP, and per-path rejection tests all fail against the unfixed code.