---
title: "Threat model — Webhooks for Laravel | Roundly"
description: "What the package defends against and how — forgery, replay, enumeration, exhaustion, SSRF, secret disclosure — and what it deliberately does not."
url: https://roundly-consulting.com/open-source/docs/webhooks-for-laravel/threat-model
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Webhooks for Laravel](https://roundly-consulting.com/open-source/docs/webhooks-for-laravel.md)

# Threat model

| Threat | Defence |
| --- | --- |
| Forged inbound requests | HMAC-SHA256 (or RFC 9421) over the id, timestamp and raw body, verified through Sentinel in constant time over every valid secret; at most 8 entries and 4 KB per header. |
| Replayed captures | 5-minute tolerance, the signed delivery id deduplicated for 7 days (+ a unique index), a body-digest guard for unsigned ids, RFC 9421 nonces recorded atomically with the stored delivery. |
| Splitting one message into many | A signed id wins over any Idempotency-Key; the same id with another body is refused. |
| Endpoint enumeration | Unknown, deleted and disabled keys answer the same 404; keys carry about 190 bits; unknown keys fire no event. |
| Resource exhaustion on the inbound route | 413 and 415 before any MAC, a rejection guard per endpoint and IP, a per-endpoint delivery limit, processing only on the queue — and the web server’s body limit for the first read. |
| A receiver answering with an endless body | Read only up to outbound.max\_response\_bytes with the guard on and off, within the 10 s budget; response\_truncated recorded. |
| Untrusted payloads | JSON to arrays only, depth ≤ 64; no unserialize, eval or extract; a sanitizer before storage. |
| Secret disclosure | Encrypted at rest, hidden, shown once, redacted DTOs, #\[SensitiveParameter\], never in events, logs, resources, about or webhooks:endpoints; wiped on delete. |
| SSRF and DNS rebinding | Safe Fetch’s guard on save and on every hop: https only, every resolved address public, app hosts blocked, the connection pinned to the vetted addresses, no proxy, redirects off or re-vetted. |
| Header injection and spoofing | Custom header names and values validated; framing, signature and proxy headers never settable. |
| Retry storms | Exponential backoff with jitter, Retry-After honoured, a per-endpoint throttle, auto-disable. |

## Not defended

- Row-swapping by a database writer — the encrypted cast is not bound to its row, so someone who can write the database can copy one endpoint’s secret ciphertext to another.
- A compromised sender or receiver — whoever holds the secret can sign.
- Clock skew — senders and receivers need NTP; the tolerance is configurable per endpoint.
- A host-wide egress proxy — the guard bypasses environment proxies; with WEBHOOKS\_SSRF\_GUARD=off there is no destination protection at all (the response cap still holds).
- Shared client IPs — behind a load balancer or a CDN without trusted proxies, a stranger’s bad requests can rate-limit the real sender for a minute.
- Queued WebhookReceived listeners — they store the sanitized payload in the queue unencrypted.
- Ordering — none between deliveries to one endpoint; receivers order by the envelope’s timestamp.
- The per-endpoint outbound throttle on the in-memory store holds per process only.

## Exceptions

In RoundlyConsulting\\Webhooks\\Exceptions, extending WebhooksException (a RuntimeException), with translated messages. Configuration values that do not read throw the toolkit’s InvalidConfigurationException:

| Exception | Thrown when |
| --- | --- |
| `EndpointNotFoundException` | find() misses; a scoped handle is given another owner’s endpoint or delivery. |
| `EndpointNotAcceptingException` | send(), ping(), redeliver() or redeliverFailed() to a disabled endpoint, send() to an unsubscribed one — reason(): disabled or not\_subscribed. |
| `InvalidEndpointException` | A bad URL, custom header, subscription list, imported secret or option. |
| `InvalidEventTypeException` | A malformed event type, or one outside outbound.event\_types. |
| `PayloadTooLargeException` | An encoded outbound body over outbound.max\_payload\_bytes. |
| `PayloadPrunedException` | Replay or redeliver of a delivery whose body retention removed. |
| `DeliveryInProgressException` | Replay of a delivery being processed; redeliver of one still pending, sending or retrying. |
| `DestinationBlockedException` | The SSRF guard refuses an outbound URL on save — reason(), blockedBy(), unresolved(). |
| `WebhookSignatureException` | A scheme refuses a request — reason(): RejectionReason; because() for your own schemes. |
| `UnknownSchemeException · UnknownHandlerException` | An unregistered name or alias, or a class that is not a scheme or handler. |
| `SecretRotationException` | A grace in the past or longer than secrets.max\_grace\_seconds. |
| `SigningUnavailableException` | Inside an attempt, for an endpoint whose scheme cannot sign — caught: the delivery fails with signing. |
| `PayloadSanitizerException` | The sanitizer threw (the pipeline answers 500 sanitizer\_failed). |
| `InvalidWebhooksConfigurationException` | A class-valued config key names no implementation; an owner whose key does not fit webhooks.key\_type. |

## Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

[More ways to support, including crypto](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/webhooks-for-laravel.md)
