Impact
The shared secret that authenticates ObjectStack to a webhook receiver is stored verbatim, per delivery attempt, in a table readable over the ordinary data API. Anyone who can read sys_http_delivery recovers the signing key for every subscriber and can mint payloads the receiver accepts as genuine — the signature is the receiver's only proof of origin.
Admin-gated on this deployment, so this is defense-in-depth rather than privilege escalation. But the blast radius is external: it compromises the trust boundary at systems ObjectStack does not control, and rotating a leaked key means re-coordinating with every receiver operator.
Root cause (located)
sys_http_delivery carries the secret as a plain column alongside headers_json; the outbox writer persists it with the attempt rather than resolving it at send time.
The platform already has the machinery to avoid this and uses it elsewhere: sys_setting stores a sec_ handle with the ciphertext in sys_secret (aes-256-gcm). And the same QA run proved the sibling datasource path gets it right — an inline secret on POST /api/v1/datasources is split out server-side, and a byte-level scan of every table and column of the running DB found no occurrence of it.
So this is an inconsistency between two sibling subsystems, not a missing capability — which is what makes it tractable.
Reproduction
- Author a webhook subscriber with a signing secret and trigger a delivery.
- Read the delivery row over the ordinary data API as an admin:
GET /api/v1/data/sys_http_delivery. - The authored secret comes back verbatim on the attempt row.
Found as an adjacent observation while verifying HMAC correctness: the runner recomputed HMAC-SHA256(raw body, <secret>) against the delivered x-objectstack-signature — exact match, so signing itself is correct — and then noticed the secret was readable from the delivery table.
Acceptance criteria
- The signing secret is not recoverable from
sys_http_delivery (or any delivery/outbox row) over the data API, for any persona. - Signing still works end-to-end: a delivered payload's
x-objectstack-signature still verifies against the subscriber's secret, asserted by recomputing the HMAC over the raw body — not by asserting "no error". - A regression guard reads the delivery table directly after a real delivery and asserts the secret's bytes are absent (the byte-level-scan shape the datasource path already passes).
Suggested shape (not prescriptive)
Store a handle rather than the value on the delivery row and resolve it at send time, matching the sys_setting / sys_secret split. Failing that, at minimum keep it off the delivery record entirely — it belongs to the subscriber, not to each attempt — and exclude the column from the data-API projection.
Worth checking in the same pass whether any other outbox/delivery table copies a credential the same way.
Note on target:<major>
Not applied here — that label has a single producer and the maintainer has not ruled on this card. Flagging the tension for that ruling rather than pre-empting it: this is admin-gated (the #7522 precedent for an admin-gated cleartext read was not boarded), but unlike #7522 its blast radius reaches outside the deployment, which is an argument for boarding it. Maintainer's call.
Provenance & disclosure
Held back from the public run card #7690 pending disclosure handling (D1 precedent); the private write-up is docs/qa/platform-checklist/FOLLOW-UPS.md §1a entry D12 (branch claude/platform-test-checklist-ocwugl @ 1ce13eb). Filed publicly under the maintainer's standing instruction of 2026-08-11 (verbatim): 「有问题就立卡片」.
Source
QA run #7690 (webhook-lifecycle, adjacent observation; framework 92f26f75), private write-up FOLLOW-UPS.md §1a D12. The webhook item itself passes — signature computation, custom headers, per-attempt durability, timeout enforcement and the retry ladder were all proven correct; this finding does not change that verdict.
Impact
The shared secret that authenticates ObjectStack to a webhook receiver is stored verbatim, per delivery attempt, in a table readable over the ordinary data API. Anyone who can read
sys_http_deliveryrecovers the signing key for every subscriber and can mint payloads the receiver accepts as genuine — the signature is the receiver's only proof of origin.Admin-gated on this deployment, so this is defense-in-depth rather than privilege escalation. But the blast radius is external: it compromises the trust boundary at systems ObjectStack does not control, and rotating a leaked key means re-coordinating with every receiver operator.
Root cause (located)
sys_http_deliverycarries the secret as a plain column alongsideheaders_json; the outbox writer persists it with the attempt rather than resolving it at send time.The platform already has the machinery to avoid this and uses it elsewhere:
sys_settingstores asec_handle with the ciphertext insys_secret(aes-256-gcm). And the same QA run proved the sibling datasource path gets it right — an inlinesecretonPOST /api/v1/datasourcesis split out server-side, and a byte-level scan of every table and column of the running DB found no occurrence of it.So this is an inconsistency between two sibling subsystems, not a missing capability — which is what makes it tractable.
Reproduction
GET /api/v1/data/sys_http_delivery.Found as an adjacent observation while verifying HMAC correctness: the runner recomputed
HMAC-SHA256(raw body, <secret>)against the deliveredx-objectstack-signature— exact match, so signing itself is correct — and then noticed the secret was readable from the delivery table.Acceptance criteria
sys_http_delivery(or any delivery/outbox row) over the data API, for any persona.x-objectstack-signaturestill verifies against the subscriber's secret, asserted by recomputing the HMAC over the raw body — not by asserting "no error".Suggested shape (not prescriptive)
Store a handle rather than the value on the delivery row and resolve it at send time, matching the
sys_setting/sys_secretsplit. Failing that, at minimum keep it off the delivery record entirely — it belongs to the subscriber, not to each attempt — and exclude the column from the data-API projection.Worth checking in the same pass whether any other outbox/delivery table copies a credential the same way.
Note on
target:<major>Not applied here — that label has a single producer and the maintainer has not ruled on this card. Flagging the tension for that ruling rather than pre-empting it: this is admin-gated (the #7522 precedent for an admin-gated cleartext read was not boarded), but unlike #7522 its blast radius reaches outside the deployment, which is an argument for boarding it. Maintainer's call.
Provenance & disclosure
Held back from the public run card #7690 pending disclosure handling (D1 precedent); the private write-up is
docs/qa/platform-checklist/FOLLOW-UPS.md§1a entry D12 (branchclaude/platform-test-checklist-ocwugl@1ce13eb). Filed publicly under the maintainer's standing instruction of 2026-08-11 (verbatim): 「有问题就立卡片」.Source
QA run #7690 (
webhook-lifecycle, adjacent observation; framework92f26f75), private write-up FOLLOW-UPS.md §1a D12. The webhook item itself passes — signature computation, custom headers, per-attempt durability, timeout enforcement and the retry ladder were all proven correct; this finding does not change that verdict.