You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Filed unassigned as an out-of-scope finding from #7722 / PR #7798. Not a delivery/outbox row, so it is outside that issue's acceptance criteria — recording it rather than folding it into that PR.
What
A webhook subscriber's HMAC signing secret is persisted verbatim inside the definition_json textarea on sys_webhook:
packages/plugins/plugin-webhooks/src/bootstrap-declared-webhooks.ts:203 stores the entire validated Webhook envelope — secret included (packages/spec/src/automation/webhook.zod.ts:201) — as definition_json: JSON.stringify(wh).
auto-enqueuer.ts:parseRow reads it back out (defn.secret) to sign deliveries.
sys_webhook is an admin-authorable object with no restrictive enable.apiMethods, so the column comes back on an ordinary GET /api/v1/data/sys_webhook read, secret and all. Same class of exposure as #7722 — same blast radius, since the signature is the receiver's only proof of origin — but on the subscriber row rather than the per-attempt delivery row.
#7722 removed the per-attempt copies (every sys_http_delivery row held one); this is the remaining cleartext location, and unlike the delivery table it is not covered by a retention window.
Why it is a separate card
The delivery-row fix could be made without a secret store at all: a delivery's body is fixed at enqueue, so the outbox signs once and keeps only the signature. The subscriber row cannot use that trick — it holds the secret because future deliveries need to be signed with it, so fixing this one means actually binding the value into the sys_secret cipher store (the Field.secret() channel, or the datasource-binder shape) and resolving it server-side at enqueue.
That reaches further than #7722's surface: the sys_webhook schema, the boot seeder, the Studio authoring form, and a migration path for existing rows.
Suggested shape (not prescriptive)
Move secret out of the definition_json blob into its own Field.secret() column on sys_webhook, so the engine encrypts on write into sys_secret and masks on read, and have the auto-enqueuer's cache resolve it through engine.resolveSecret() when it builds an enqueue input. Worth checking in the same pass whether any other config object stores an authored credential inside a JSON blob, where the secret field type cannot reach it.
Acceptance criteria
The signing secret is not recoverable from sys_webhook over the data API, for any persona.
Signing still works end-to-end: a delivered payload's x-objectstack-signature verifies by recomputing HMAC-SHA256 over the raw body (not by asserting "no error").
Filed unassigned as an out-of-scope finding from #7722 / PR #7798. Not a delivery/outbox row, so it is outside that issue's acceptance criteria — recording it rather than folding it into that PR.
What
A webhook subscriber's HMAC signing secret is persisted verbatim inside the
definition_jsontextarea onsys_webhook:packages/plugins/plugin-webhooks/src/bootstrap-declared-webhooks.ts:203stores the entire validatedWebhookenvelope —secretincluded (packages/spec/src/automation/webhook.zod.ts:201) — asdefinition_json: JSON.stringify(wh).auto-enqueuer.ts:parseRowreads it back out (defn.secret) to sign deliveries.sys_webhookis an admin-authorable object with no restrictiveenable.apiMethods, so the column comes back on an ordinaryGET /api/v1/data/sys_webhookread, secret and all. Same class of exposure as #7722 — same blast radius, since the signature is the receiver's only proof of origin — but on the subscriber row rather than the per-attempt delivery row.#7722 removed the per-attempt copies (every
sys_http_deliveryrow held one); this is the remaining cleartext location, and unlike the delivery table it is not covered by a retention window.Why it is a separate card
The delivery-row fix could be made without a secret store at all: a delivery's body is fixed at enqueue, so the outbox signs once and keeps only the signature. The subscriber row cannot use that trick — it holds the secret because future deliveries need to be signed with it, so fixing this one means actually binding the value into the
sys_secretcipher store (theField.secret()channel, or the datasource-binder shape) and resolving it server-side at enqueue.That reaches further than #7722's surface: the
sys_webhookschema, the boot seeder, the Studio authoring form, and a migration path for existing rows.Suggested shape (not prescriptive)
Move
secretout of thedefinition_jsonblob into its ownField.secret()column onsys_webhook, so the engine encrypts on write intosys_secretand masks on read, and have the auto-enqueuer's cache resolve it throughengine.resolveSecret()when it builds an enqueue input. Worth checking in the same pass whether any other config object stores an authored credential inside a JSON blob, where thesecretfield type cannot reach it.Acceptance criteria
sys_webhookover the data API, for any persona.x-objectstack-signatureverifies by recomputing HMAC-SHA256 over the raw body (not by asserting "no error").sys_http_deliveryrow #7722's at-rest test and the datasource path both use.