Skip to content

T99: accept a webhook delivery once - #14

Open
tomj12k wants to merge 5 commits into
mainfrom
security/t99
Open

tomj12k wants to merge 5 commits into
mainfrom
security/t99

Conversation

@tomj12k

@tomj12k tomj12k commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Tracked as T99 in the private Morelitea/security repository.

The finding, reasoning, verification, and review notes live there. This description is deliberately minimal because this repository is public.

Scope: the delivery lifecycle, its schema step, and observable behavior coverage. The branch includes the amended #15 dependency.

Verified: the exact head passes 303 tests, typecheck, production build, and hosted checks. Reviewers should use T99 / WI-27 and WI-31 in Morelitea/security for detail.

tomj12k and others added 3 commits September 11, 2026 07:06
Tracked as T99. Details in the private security repository.

Adds a webhook_deliveries table, claimDelivery(), and a check on the webhook
route between signature verification and handling.

NOTE FOR REVIEW -- this changes the schema fingerprint in src/db.ts, and that
file has no migration path. Applying it to an existing database produces:

  SchemaMismatchError: this database was built by a different version of
  src/db.ts ... There is no migration path: drop the database and let it be
  recreated, or reconcile the difference by hand and update schema_version.

So this cannot ship to a running deployment as it stands. The code is
finished and tested; the blocker is the schema mechanism, and that deserves
its own decision rather than being worked around here.

Verified on a fresh database, against main on the same fresh database:

  main        27 failed | 258 passed
  this branch 27 failed | 262 passed

Same 27 pre-existing failures, plus the 4 new tests. Those 27 fail on main
too and are unrelated -- they appear to want database state a fresh schema
does not have.

test/schema.test.ts enumerates the tables the app creates and needed the new
one added; that is the test doing its job.

tsc reports 23 errors on this branch and 23 on main, same distribution.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D5xdKhPXJT4HMWwUyENiu1
Review found this PR describing the weakness three times in a public
repository, plus two ways around the fix -- which is the exact thing the
estate masking policy exists to stop, in a change I wrote after setting it.

Removed from src/server.ts, src/db.ts and the test header:

  - how a captured delivery verifies again (three times, near-identically)
  - that placing the check before signature verification would let anyone
    fill the table with ids of their choosing
  - that answering non-2xx would make GitHub redeliver what was refused

Each is now the invariant instead. The server comment is four lines:

  A delivery id is accepted once. After the signature check, so only GitHub
  can create rows. Answer 2xx either way -- a non-2xx asks GitHub to
  redeliver. Rationale: T99.

That says everything a maintainer needs to keep the property, and nothing a
reader needs to defeat it.

The test file is renamed delivery-once.test.ts. "replay" in a filename is a
smaller hint than the prose was, but the branch-name rule is that an id is the
only thing a public name should carry, and a file name is read the same way.

Verified: delivery-once.test.ts, 4 passed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D5xdKhPXJT4HMWwUyENiu1
Replaces the single schema fingerprint with an ordered list of named
steps, recorded one row per step. An unapplied step is applied; an
applied step can never change. Databases stamped by the previous scheme
are adopted in place, with their rows.

Adds a guard that refuses an unmarked data-destroying statement, and
tests covering both directions.

Details are tracked internally as T99 / WI-31.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D5xdKhPXJT4HMWwUyENiu1
Require each migration step to classify its data risk explicitly. Mark post-initial schemas so the previous single-fingerprint build refuses an unsafe rollback.
Merge the forward schema path and add the delivery ledger as immutable step 0002. Use expiring attempt leases so failed or interrupted work remains retryable while completed deliveries are suppressed.
@tomj12k

tomj12k commented Sep 13, 2026

Copy link
Copy Markdown
Contributor Author

T99 exact-head follow-up is in 4395fe4, including the amended #15 dependency.

The behavior cases were captured before implementation and pass afterward. Final evidence: 18 schema cases, 7 focused delivery cases, 303 full tests, typecheck, production build, and hosted checks pass.

Detailed findings, trade-offs, and uncertainties are recorded under private T99 / WI-27 and WI-31 in Morelitea/security.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant