Repository navigation
1.8.0
Fixed — the inbound endpoint was published on a URL nobody could send to
The endpoint answered on /!/webhooks/inbound/{handle}. !/ is Statamic's prefix for its own utility routes; it is not a string anyone types into a provider's webhook field, and every other place in this addon that named the prefix already spelled it without one — InboundController passed webhooks/inbound to the CP, SettingsController reported /webhooks/inbound, and only config/webhook-manager.php said !/webhooks/inbound. Two of those three were wrong about the addon's own routing table.
A POST to the URL those defaults implied did not 404. It fell through to Statamic's front-end catch-all Route::any('/{segments?}'), which is in the web middleware group, which contains ValidateCsrfToken — so it came back 419 “CSRF token mismatch”, with or without credentials, identically for a correct token and a forged one. That is a convincing impersonation of a broken authentication layer, and it is why this was diagnosed as a CSRF problem on the endpoint. The endpoint was never reached.
The canonical prefix is now webhooks/inbound. !/webhooks/inbound stays routable through inbound.legacy_route_prefixes, so a sender already configured against the old URL keeps working; set that config key to [] once none is. The CP, the settings screen and the router now all read the same value, so the URL an operator copies out of the CP is the URL that is routed.
Changed — the inbound endpoint no longer runs the web middleware group
It was registered through Statamic's $routes['web'] hook, which drops every route it registers into the application's web group. For a machine-to-machine endpoint that group is wrong end to end: ValidateCsrfToken rejects a sender that by definition has no session token, StartSession writes a session file per delivery that nothing ever reads, and EncryptCookies, ShareErrorsFromSession and whatever the host app appends — Inertia handling, last-seen writes, redirect handlers — all run on a request that will only ever be answered with JSON.
The previous fix for this was ->withoutMiddleware([ValidateCsrfToken::class]) on the route. That removes exactly one member of the group, and only for as long as the host application registers that precise class name; a host on the older VerifyCsrfToken alias, or one that subclasses it, gets a route that is silently back under CSRF. The endpoint is now registered by WebhookManagerServiceProvider::bootInboundRoutes() outside Statamic's hook, declaring its complete stack (SubstituteBindings) rather than inheriting one and undoing a piece of it. Nothing is removed because nothing is inherited.
Registration still happens during bootAddon(), which Statamic fires from its $app->booted() callback before it loads its own route files — so the endpoint is still matched ahead of the front-end catch-all, exactly as it was.
The stack is webhook-manager.inbound.middleware and it is the complete list, not an addition to web. Putting 'web' back in it restores the 419.
Fixed — basic auth authenticated everyone when it was not configured
BasicAuthVerifier::verify() compared the configured username and password against the request's with hash_equals — correct, constant-time — but did not first check that anything was configured. hash_equals('', '') is true, and a request with no Authorization header has an empty user and an empty password, so an endpoint saved with auth_type = basic and an empty or half-filled auth_config passed both comparisons and went on to parsing, mapping and action dispatch. It now fails closed unless both a username and a password are set.
No endpoint on any environment we can see uses basic; the one configured inbound endpoint uses static_header, which was never affected. The exposure mattered more the moment the route stopped sitting behind web, since the verifier is now the only thing in front of it.
Known — configured but not enforced
Both entries below were fixed after this release; see the Unreleased section at the top of this file. They are left here because the 1.9.0 tag still behaves as described.
Two columns on webhook_inbounds are accepted by the CP, validated, cast and stored, and then never consulted at request time:
rate_limit_config— there is no rate limiting on the inbound endpoint.webhook-manager.inbound.rate_limit_per_minuteis likewise only ever read back out to the settings screen. An endpoint is as available as the webserver in front of it.IpAllowlistVerifier— implemented, correct, and never passed toAuthSchemeRegistry::registerDefaults(), soip_allowlistcannot be selected or matched.
replay_protection_enabled, max_payload_kb, allowed_methods and expected_content_type are enforced, in InboundRequestProcessor, in that order, before the action dispatcher runs. These two are not, and are listed here rather than quietly implemented.