Skip to content

fix(security): prevent outbound webhook SSRF and DNS rebinding #551

Description

@seonghobae

Verified protected-develop defect

Fresh protected develop@1fadec04195805722829b386475a09a15f8cd926 accepts an owner/admin supplied outbound webhook URL with only /^https?:\/\//, persists it, and later passes that URL directly to server-side fetch() in sendWebhook(). A tenant administrator can therefore direct ScopeWeave at loopback, RFC 1918, IPv4 link-local / metadata, IPv6 loopback/link-local/ULA, IPv4-mapped IPv6, or a hostname that DNS-rebinds to those targets. The current fetch client also owns redirect behavior rather than a fail-closed webhook transport boundary.

Protected evidence:

  • server/app.mjs webhook creation accepts any http(s) string;
  • sendWebhook() issues the POST directly to the persisted URL;
  • no destination DNS/IP authorization or pinned lookup is performed before network I/O.

This is a buyer-visible tenant-isolation / cloud-control-plane SSRF defect, not merely hardening.

Required repair contract

Implement this as one bounded outbound-webhook transport slice. Preserve current webhook signing, event filtering, retry accounting, secret non-disclosure, and ordinary public HTTPS delivery.

  1. RED first: add realistic regressions proving protected behavior is unsafe before production changes. Cover direct IPv4/IPv6 loopback, RFC 1918, IPv4 link-local (including metadata-style addresses), IPv6 link-local/ULA, IPv4-mapped IPv6, localhost-like names, DNS answers that are private, mixed public+private answer sets, and DNS rebinding between attempts. Include a real public-IP-shaped deterministic success seam without external network dependence.
  2. Parse with URL; production webhook destinations are HTTPS. Reject credentials and fragments. Keep normal path/query support. If explicit development HTTP compatibility is genuinely required, limit it to SCOPEWEAVE_DEV=1 plus loopback only and test that production never inherits it.
  3. Resolve A/AAAA before connection and fail closed unless every candidate address is permitted public-unicast destination authority. Treat special-use/non-routable/private/link-local/loopback/unspecified/multicast/broadcast/documentation/benchmark/reserved ranges and IPv4-mapped forms conservatively.
  4. Prevent DNS pinning/rebinding TOCTOU: the network connection must use a lookup/address that is cryptographically/TLS-hostname compatible with the original hostname but pinned to a destination that was just validated. Validation followed by an unrelated second resolver decision is not sufficient. Node's HTTPS request lookup seam can be used to bind the validated address while preserving the original hostname/SNI.
  5. Do not follow redirects. A redirect response is a delivery failure and must never transplant the webhook secret/signature/body to another authority.
  6. Keep the existing 3-second request budget and bounded retry semantics unless tests prove a smaller root-cause change is necessary. Each retry must freshly resolve, validate, and pin its destination; never reuse a later unvalidated DNS result.
  7. Keep delivery recording stable and non-secret. Resolver/TLS/transport failure must not surface internal addresses or credentials to customer-visible error bodies/logs.
  8. Put destination-policy and transport logic in beginner-readable testable production modules rather than expanding the monolithic route with untestable inline branches. Register every owned module/test in canonical unit and c8 coverage and maintain exact 100% owned statement/branch/function/line coverage where tooling exposes it.
  9. Add doctoring/traceability with APA 7 references and explicit active-PR vs protected-shipped truth. Primary implementation basis: OWASP SSRF Prevention Cheat Sheet (custom webhooks; redirect disabling; A+AAAA validation / DNS pinning), Node.js 22 HTTPS/HTTP custom lookup support, RFC 1918 private IPv4, RFC 3927 IPv4 link-local, RFC 4193 IPv6 ULA, and current IPv6 special-use guidance.
  10. Do not touch central .github, auth/session semantics, Clearfolio, Stripe inbound-webhook code, dependency lockfiles, branch protection, or unrelated buyer slices.

Acceptance

  • Existing broad webhook API behavior remains functional for valid public HTTPS endpoints.
  • No server-side connection is attempted to a denied destination, including after DNS changes or redirect responses.
  • Exact deterministic RED→GREEN evidence is preserved in PR history/description.
  • Current protected base is refetched before branch creation/push; no force-push/destructive rebase.
  • Fresh exact-head repo CI/browser/coverage/SAST/security/dependency/supply-chain and live organization gates after every head move.
  • No model/status-only evidence is treated as independent approval; do not merge until live governance is genuinely satisfied.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: authAuthentication, authorization, identity, or tenant isolationarea: ci-cdCI, GitHub Actions, checks, release, or supply chainarea: dependenciesDependency or lockfile maintenancearea: securitySecurity boundary, hardening, or vulnerability preventionbugSomething isn't workingpriority: mediumNormal-priority or P2 workstatus: triagedOpen issue has an organization taxonomy assignmenttype: bugDefect or incorrect behavior

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions