Stop target credentials leaking into event databases (closes #206)
Some checks failed
check / check (push) Failing after 2m36s

A Delivery carries its Event and Target structs in memory for the
delivery engine, so GORM's automatic association save upserted the
whole target row -- config included, which holds destination URLs
and bearer credentials -- into the per-webhook event database with
an empty webhook_id. Event databases are the files most likely to be
backed up or handed to someone else, so they shipped the credentials
with them.

Register a create and update callback on every per-webhook
connection that omits associations, rather than fixing the one call
site: it covers writes inside a transaction and write paths added
later. Sweep any rows already written, before the migration on each
open, so it is idempotent and a no-op on a database with no targets
table.

Encryption of target config at rest in webhooker.db is deliberately
not part of this: it is tracked separately.
This commit is contained in:
2026-08-20 04:17:50 +00:00
committed by sneak
parent aba02bc509
commit 186daabe22
5 changed files with 795 additions and 8 deletions

View File

@@ -506,14 +506,28 @@ backups at rest and restrict who can read them.
- `events-{uuid}.db` and `archive-{uuid}.db` hold the **full payload
body and headers** of every event as received, including whatever the
sending service put in them — tokens, signatures, personal data.
- Until
[issue #206](https://git.eeqj.de/sneak/webhooker/issues/206) is fixed,
the event databases **also contain target credentials**: a GORM
association upsert on the delivery and retry write path copies
`targets` rows, `config` included, into the per-webhook database. For
a Slack target the `webhookUrl` *is* the bearer credential, and an
`http` target's URL can embed userinfo. Handing someone an
`events-*.db` today hands them live delivery destinations.
- Event databases written before
[issue #206](https://git.eeqj.de/sneak/webhooker/issues/206) was fixed
**also contain target credentials**: a GORM association upsert on the
delivery and retry write path copied `targets` rows, `config`
included, into the per-webhook database. For a Slack target the
`webhookUrl` *is* the bearer credential, and an `http` target's URL
can embed userinfo. This version never writes those rows; the first
time it opens such a file it deletes them and vacuums the file, which
removes the credential bytes rather than only unlinking the rows.
Deleting alone would not: the bytes stay readable in the file's free
pages until it is rewritten. The sweep is recorded in the file's
`user_version` only once the vacuum returns, so a sweep that fails or
is interrupted fails the open and is retried on the next one, and a
file this version has opened without error holds no leaked rows and
no recoverable bytes from them. On upgrade this rewrites each
existing `events-{uuid}.db` once, on its first open. Two cases still
hand over live delivery destinations: a backup taken from an older
build, and a backup of a file this version has not yet opened
successfully. Copies already made stay affected — the sweep only
rewrites the file it opens, and freed blocks may persist in
filesystem snapshots and on the underlying storage. Rotate any target
credential that was in a backup you cannot account for.
- `webhooker.db` stores target config **unencrypted**, tracked at
[issue #212](https://git.eeqj.de/sneak/webhooker/issues/212), next to
the session encryption key and the Argon2id password hashes.