Skip to content

Build1 publisher3 min readPublished

Filament Studio's webhook endpoints verified HMAC signatures against an empty secret

Filament Studio's developer stopped trusting his 1,800 passing tests in September and installed the package into a real Laravel app. The defects he logged share one shape: success reported, nothing done.

The Engineer · Build desk

Illustration accompanying Filament Studio's webhook endpoints verified HMAC signatures against an empty secret

What happened

  • Mutation testing on Filament Studio's Flows module ran to an 80% MSI target per module, on top of a suite that reached about 1,800 tests by the end of August.
  • In September the developer installed the package into a separate Laravel app and ran it on MySQL with a real queue worker, real HTTPS webhook deliveries and the admin panel in a browser.
  • That pass produced fourteen defects in v1.8.0 and one more in v1.7.1, and the existing suite had covered none of them.
  • A seeded "Welcome Email" flow reported Completed with a duration of 0 ms and no steps, even though nothing had run.
  • Flows created through the designer never got a webhook secret, because the only code path that generated one looked for an auth_mode field the designer never rendered.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • exposure The people carrying this are Flows users with a publicly reachable webhook URL, and the upgrade does not quietly repair them: affected flows return 401 until someone republishes each one to generate a secret.
  • constraint A per-module mutation score cannot reach a disagreement between a column default and a config schema, because neither module's code is wrong on its own, so the metric cannot measure this class of defect at all.
  • decision Any team whose fixtures are built by the same builder its UI calls now has a choice to make about adding a second fixture population from the API and seeder path, since that is the population that broke the graph walk.
  • cost Reproducing this kind of pass costs a second application, a MySQL instance, a queue worker and real HTTPS delivery, and the maintainer pays that before any user does.

The graph walk asked for successors with `$matches = $matches->where('sourceHandle', $branch)` [8]. Nodes drawn in the visual designer carry named success and failure handles, so that comparison always matched [9]. Edges written by hand, by a seeder, or posted through the REST API look like `{ "source": "trigger", "target": "send_email" }`, with no handle, so the walk stopped at the trigger node and finished, and "finished with nothing to do" was recorded as success [10]. The current code defaults the missing value: `($edge['sourceHandle'] ?? 'success') === $branch` [11].

"Every graph in my tests had the shape the designer produces. The engine had only ever been tested on input from its own UI," Fedorenko wrote [12].

The webhook bug needed five conditions in a line:

1. The flow table's `webhook_auth_mode` column defaults to `hmac` [14]. 2. The secret was generated only when the trigger node's config contained `auth_mode: hmac` [15]. 3. The trigger's config schema never declared an `auth_mode` field, so the designer never rendered it and nobody could set it [15]. 4. `webhook_secret` therefore stayed NULL on the normal path [16]. 5. The verifier took the secret as a string, and `(string) null` is `''` [16].

The sender is meant to sign `{timestamp}.{body}` with a shared secret [13]. Any caller can compute that HMAC against an empty one, so any caller held a valid signature [17].

The verifier now refuses an empty secret and throws `InvalidWebhookSignatureException('Webhook secret is not configured for this flow.')` [18]. The second half of the fix moved auth mode, secret, API-key allowlist and redaction paths onto the flow record, and the webhook trigger node has no config at all [19]. "The real bug was having two places to set one security setting. Each had its own tests, and nothing checked that they agreed," Fedorenko wrote [20].

The 80% MSI figure cannot reach this [2]. A mutation score is a claim about mutants inside one module, and neither module here was wrong on its own: the column default was `hmac` [14], the node-config branch generated a secret when asked [15]. For the score to have said anything about this endpoint, the fixtures would have had to create a flow row the way the designer creates one and then post a request signed with an empty string.

Neither of the two defects Fedorenko details turns on MySQL or the queue worker. The real environment is what surfaced them; the cause in both cases was that the test inputs were produced by the same code the tests were checking. He describes the post as going through the interesting ones of the fourteen [4].

For anyone already running Flows with a reachable webhook, Fedorenko says to look for flows where `webhook_auth_mode = 'hmac'` and `webhook_secret IS NULL`, and that those endpoints were effectively public [21]. In the same area, `webhook_redact_paths` scrubbed the parsed body but not the raw copy stored next to it, so a redacted value still sat in `trigger_payload.raw`; the raw body is now re-encoded from the sanitized one [23].

What to watch

  • Whether later releases add test fixtures built from the REST API and seeder paths instead of designer output.
  • Whether the MCP server, which also creates records without the designer, produces handle-less graphs of the same kind.
  • Whether the empty-secret verification path gets a published advisory so affected installs know to run the query.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories