All packages
Webhooks for Laravel
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 cryptoBy donating, you agree to our donation terms.
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.