Webhook security

Outbound webhooks: pin the checked destination

An outbound webhook must connect to an address approved for that delivery attempt. Registration checks alone leave DNS changes and redirects outside the guarantee.

Policy cases
16 cases: 2 allowed and 14 denied
DNS example
Mocked resolution changes after validation
Redirects
2 target plans; no HTTP redirect executed
Boundary
Offline policy model; no DNS, TLS or sockets
A delivery envelope approaches a guarded routing junction where one path points outward and another enters a protected enclosure.
Conceptual illustration of choosing an outbound webhook destination. The article tests an offline connection policy.

Start with the address the worker will actually connect to

A customer's webhook URL is an instruction to your delivery worker. If that worker can reach a private service, a callback such as https://private.example/hook can turn an integration feature into an unwanted request inside your network. Permission to administer a SaaS workspace does not imply permission to choose every destination your infrastructure can reach.

Checking the URL at registration leaves another boundary exposed. The DNS answer can change before delivery, and an HTTP redirect can introduce a different destination. The connection must use an address approved for that attempt. Keep automatic redirects disabled unless the product deliberately supports them and validates each hop.

The downloadable model makes that boundary inspectable. It runs 16 URL cases, allowing two and rejecting 14 under a stated policy. A separate counterexample changes a mocked DNS answer between validation and connection. The unsafe path selects a private address while the pinned plan retains the approved public address. These are offline checks, not requests against a production system.

Download the Python model and executed results. Run python3 experiment.py in a writable directory. The script opens no sockets and performs no DNS queries. The example hostnames and addresses represent inputs to a policy, not endpoints contacted by the script.

Write the callback policy before choosing an HTTP client

This walkthrough uses a deliberately narrow contract: HTTPS, port 443, an ASCII hostname or supported IP literal, no embedded credentials and no fragment. It also rejects control characters and backslashes before parsing. Some of these restrictions are product choices. A fragment is not transmitted as part of an HTTP request, but rejecting it avoids storing a callback whose visible text suggests a server-side meaning the fragment does not have.

Do not treat URL user information as webhook authentication. A callback containing user:secret@host puts a secret into a value that might appear in logs, support tools or administrative screens. Store an endpoint's credential separately and define which outbound request may receive it. If the product needs alternate ports or internationalized hostnames, extend the contract with explicit parsing and test cases rather than accepting them accidentally.

For a known partner set, a destination allowlist can narrow the job further. An open integration product may need arbitrary customer-controlled public endpoints. Those are different policies: broad public reachability still permits calls to services you would not authorize through a partner allowlist.

OWASP's SSRF prevention guidance treats user-selected webhooks as a relevant case and recommends controls at both the application and network layers. Its webhook security guidance calls for HTTPS and destination checks for publishers. Translate that guidance into a contract your delivery path can enforce.

URL contract, complete DNS answer policy and pinned connection plan precede a request. The original DNS hostname remains the TLS identity.
Figure 1. Proposed worker contract. This diagram describes the transport boundary; the fixture does not implement an HTTP client. View full-size figure.

Inspect every address in the resolution result

Suppose mixed.example resolves to 8.8.8.8 and 10.0.0.8. Screening only the first result creates an unresolved question: which result will the client actually try? The fixture rejects the entire answer set if any candidate fails policy. That is a conservative selection rule, and its availability cost is explicit: one blocked record rejects an otherwise usable public record.

Empty resolution results fail closed. IPv6 needs its own rules. Blocking private IPv4 while leaving IPv6 unclassified does not implement the same destination policy. The fixture rejects IPv6 loopback, IPv4-mapped IPv6 and documentation addresses. It permits only its supported global-unicast envelope after applying its listed exclusions.

The code does not delegate this decision to ipaddress.is_global. It lists network rules so the educational output does not change when a runtime changes classification tables. The IANA IPv4 registry and IPv6 registry document special-purpose allocations and their reachability attributes. The model's rules are a conservative subset for these cases, not an exhaustive implementation of those registries.

Production classification needs a maintained source of address policy and your own infrastructure exclusions. A globally routable address can still belong to a service the worker must not call. Conversely, a conservative special-purpose exclusion can block a legitimate destination. Record the denied category so an operator can explain the decision without weakening it through an undocumented exception.

Run the negative cases before adding transport code

The fixture separates URL parsing from address policy and returns a connection plan only after both succeed. Its result contains the original hostname, the checked address and the intended HTTPS port. Inspect that object before wiring an HTTP client to it.

Executed URL policy groups and their distinct rejection boundaries
Input groupExecuted resultReason to keep the case
Public IPv4 name and public dual-stack name2 allowed plansConfirms the model does not reject every callback
Loopback, private, metadata, carrier NAT and excluded IPv6 cases7 rejectionsExercises distinct destination categories
Mixed and empty DNS answers2 rejectionsChecks the whole answer set and failed resolution
HTTP, credentials, fragment, alternate port and backslash5 rejectionsKeeps the stored URL within the supported contract

Counts in the table come from the executed fixture. No acceptance row establishes that an HTTP library respects its plan. A model that returns connectAddress has only described the required connection. The integration test must demonstrate that the transport uses it.

Sixteen offline URL cases: two allowed connection plans, fourteen denials.
Sixteen URL-policy decisions. Additional DNS, redirect and literal-plan assertions do not perform network, TLS or HTTP operations. View full-size figure.

Now inspect the rebinding counterexample in results.json. The validation address is 8.8.8.8. A later mocked resolution supplies 10.0.0.8. The vulnerable connection selects the latter. The pinned plan keeps 8.8.8.8. Both paths start from the same accepted URL, so another URL-string check would not distinguish them.

A Bugsink security advisory describes this kind of separation: validation resolved a hostname, while a later request performed another lookup. The advisory's impact was bounded by the affected webhook behavior. It provides evidence for that failure mechanism, not a claim that every webhook library has the same bug.

Carry the approved address into the connection

Bind transport selection to the validated answer. Avoid an integration in which validate(url) runs first and an unrelated post(url) resolves the hostname again. A custom resolver, a dialer or an egress service can enforce the connection boundary, depending on the transport. The relevant acceptance evidence is the address of the connection actually used.

For a DNS hostname, retain that hostname for TLS Server Name Indication, certificate hostname verification and HTTP authority. Replacing the URL's hostname with an IP address can change those behaviors. Never compensate by disabling certificate verification. For an IP literal, the plan omits DNS SNI and declares an IP certificate reference. IPv6 HTTP authority retains its brackets. Separate IPv4 and IPv6 literal assertions check those plan fields, but the fixture performs no TLS handshake for either form.

Connection pools deserve a separate test. An HTTP client may reuse an earlier connection without calling the resolver you expect on the new attempt. Ensure reuse still satisfies the endpoint and egress policy. If a proxy selects the final destination, checking the worker's direct peer alone only proves which proxy it reached. Put the destination policy at the component that opens the destination connection.

Retries are new delivery attempts. Reapply policy when creating their plans, while retaining the webhook event identity used for duplicate handling. Endpoint edits should carry a version so queued work can follow the product's rule for old versus current destinations. The webhook duplicate example covers receiver effects. Destination approval answers a separate publisher-side question.

Keep redirects out until their contract is complete

The simpler callback policy rejects redirects. If migrations require them, resolve a relative Location against the current URL, apply the same URL and address checks, and create a new connection plan for the new target. Bound the number of hops. A public first hop must not authorize a private second hop.

The script checks two mocked targets: a redirect to private.example is denied and one to redirect.example is allowed. It does not execute an HTTP redirect, test method rewriting or forward any credentials. An allowed target in this fixture is therefore only an allowed destination plan.

Decide whether a redirected POST may change method and whether the payload is safe to send to another host. A signature authenticates the payload to a receiver. It does not authorize that receiver as a destination. Do not forward an endpoint's authentication headers to a different origin by default. URL checks alone cannot decide these data-sharing rules.

The raw-body signature walkthrough tests inbound integrity and replay handling. Pair that receiver contract with an outbound destination contract when your product both sends and receives events.

Transfer the checkpoints to the real worker

Use a controlled test environment to observe approved addresses and connected peers. Change the test resolver's answer between validation and connection, return a mixed IPv4/IPv6 answer, and provide a redirect toward a blocked test destination. The release check should show rejection or a connection to the retained approved address. A successful response alone is insufficient evidence.

Also exercise proxy configuration and connection reuse. Confirm certificate failure remains a failure. Test endpoint changes during a queued retry and inspect which endpoint version the attempt used. These integration checks are proposed work. The supplied offline model did not execute them.

Finally, constrain the worker's egress and resources. Destination policy does not limit response size, request duration or how many events a tenant can enqueue. Keep delivery timeouts, concurrency limits and response-body limits close to the transport, and ensure ordinary delivery credentials do not grant unnecessary infrastructure access. Begin implementation by making the connected address visible in your controlled test trace without logging callback secrets or payloads.

Sources

Documentation checked .

  1. OWASP: SSRF Prevention Cheat Sheet
  2. OWASP: Webhook Security Cheat Sheet
  3. Bugsink: DNS rebinding security advisory
  4. IANA: IPv4 Special-Purpose Address Space
  5. IANA: IPv6 Special-Purpose Address Space
  6. IETF RFC 6066: DNS SNI excludes IP literals
  7. IETF RFC 9525: TLS service identity

Continue the conversation

Comments (1)

  1. Dreamtsoft Editorial

    Editorial follow-up: the returned connection plan is not evidence of a real network connection. Observe the connected peer, certificate checks and proxy behavior in a controlled integration environment. Keep callback secrets out of that trace.

Leave a comment

Your name and comment stay in this page and are cleared after the spam check.

10–2,000 characters. Keep the discussion relevant to this article.

Spam protection verification
Spam protection loads when you begin the form.

JavaScript is required to use this form and its spam protection.