Skip to content

fix(cdp): sign the body webhooks send and accept hmac-sha256 signing - #127117

Open
Silthus wants to merge 15 commits into
masterfrom
feat/webhook-body-hmac-signing
Open

Silthus wants to merge 15 commits into
masterfrom
feat/webhook-body-hmac-signing

Conversation

@Silthus

@Silthus Silthus commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Problem

  • A signed GET or HEAD webhook never verifies at the receiver. The signature covers the configured body, which a GET or HEAD never sends.
  • It hits webhook steps and destinations that sign with Standard Webhooks, and AWS SigV4 destinations.
  • Separately, #135206 adds an HMAC-SHA256 signature format. Every worker must accept it before any template asks for it.

Moved from the fork PR #126752, which could not run CI.

Worth: W-20261113-webhook-signed-get-head

Note

🔵 Merge risk: low · two-way door until a function first sends hmac_sha256, from custom Hog or the #135206 template: then its receivers depend on the X-Hub-Signature-256 contract, and queued retries carry the option.

  • Signed GET and HEAD webhooks, both regions: the signature now covers the empty body sent, so a verifying receiver accepts them.
  • Signed and unsigned POST and PUT on the shared CDP fetch path, used by every destination: unchanged. The shadow diff and receiver tests cover them.
  • Fetches that combine SigV4 with a webhook signer, reachable only from custom Hog: SigV4 now also signs the webhook headers. Both signatures verify in the shadow diff.
  • The new hmac_sha256 fetch option: no template sets it until #135206. Custom Hog that passes it now gets a signed POST or PUT, where today's worker ignores it and sends it unsigned. A request with no body, or a secret under 24 bytes, now fails with a logged error instead.
  • Switch-off: none: live on deploy, fix forward.
  • Tier: critical (a: nodejs/src/cdp/services/hog-executor-async.service.ts, nodejs/src/cdp/async-functions/, nodejs/src/cdp/utils/, nodejs/src/cdp/schema/)

Changes

  • Every signer signs the body actually sent: SigV4, Standard Webhooks and the new HMAC-SHA256.
  • The executor accepts an hmac_sha256 fetch option. It signs each attempt with X-Hub-Signature-256: sha256=<hex>, so the queue payload holds only a secret reference.
  • HMAC-SHA256 refuses a request with no body, whose signature would be the same on every request, and a secret under 24 bytes.
  • One fetch call mints one webhook-id, shared by every signer it uses and stable across retries.
  • SigV4 signs last, so it covers the headers the webhook signers set.
  • Nothing a user sees changes in this layer. The screenshot of the new choice moved to feat(cdp): let the webhook step sign bodies with hmac-sha256 #135206.

How did you test this code?

Test seams: the executor fetches from a local receiver that checks the bytes it got, for POST, GET, HEAD and a retry.

  • The receiver tests fail if a signer signs the configured body of a GET or HEAD, or if SigV4 runs before a webhook signer.
  • They also fail if HMAC-SHA256 sends a request with no body or a secret under 24 bytes, or rejects a 24-byte secret.
  • Hog code that calls fetch runs through the executor to the receiver. The tests fail if two calls share a webhook-id, or if both signers of one call don't sign the same one.
  • Shadow diff, base against HEAD, eight signer modes by four methods, combined signers included. Only GET and HEAD signatures, HMAC rows and SigV4 combined with a webhook signer change. Every HEAD signature verifies, and HMAC GET and HEAD send nothing.
  • Cross-version: base payloads verify on HEAD. A base worker sends an HMAC payload unsigned, so the template choice waits in feat(cdp): let the webhook step sign bodies with hmac-sha256 #135206.
  • Live run through the CDP consumer and Cyclotron worker to a verifying receiver: retries keep one webhook-id, and a wrong secret gets a 401. HMAC GET, HEAD and a short secret send nothing and log why.
  • Prod shape: counted the stored GET and HEAD webhook configs in US and EU. The counts stay private.

Release status

  • No feature flag controls this change

Docs update

  • None.

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Agent: Claude Code, Claude Opus 5.5 (claude-opus-5-5)

Skills: /writing-tests, /writing-pr-descriptions, /stacking-prs, takeover, full-stop, impact, tdd, qa-swarm, merge-risk, pr-description

Security review: not needed

More context for AI agents
  • Session: Claude Code in T3 Code. Michael's commits started this PR; a takeover session split it and fixed the review findings.
  • Decisions: see the table below.
  • Read closely: the sentBody change in hog-executor-async.service.ts, since it touches every signed GET or HEAD.
  • Follow-ups: batch-export retries rebuild the invocation and mint a new webhook-id, as Standard Webhooks already does.
Question Answer Who Where
Deploy window Split the template choice into #135206, merged after this deploys (replaces "accept unsigned sends") 👍 comment
T3 Code signing Generic webhook option 👍 comment
Fork PRs Moved upstream 👍 comment
GET and HEAD Sign the sent body 🤖 comment
Delivery id with two signers One id per fetch call, shared by its signers 🤖 comment
HMAC-SHA256 without a body Refuse and log, since the signature would never change 🤖 thread
HMAC-SHA256 secret length At least 24 bytes, as Standard Webhooks 🤖 thread

Involvement: one design pass, two review panel rounds, two upstream review rounds, three bot review rounds.

🤖 Generated with Claude Code

Silthus and others added 3 commits October 31, 2026 10:04
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Silthus Silthus self-assigned this Nov 2, 2026
@trunk-io

trunk-io Bot commented Nov 2, 2026

Copy link
Copy Markdown

Merging to master in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here

@Silthus
Silthus added this pull request to stack #127119 November 2, 2026 06:31
@github-actions

github-actions Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

🤖 CI report

✅ Trunk lane — non-backend lane

This PR is assigned to the non-backend lane. It does not run backend Python tests and may merge in parallel with PRs in other lanes.

✅ Duplication (Python) — clean

New Python code duplication introduced by this branch. Fails at 70+ tokens in app code, or 150+ tokens when both copies live in test files. Advisory while the gate proves itself: extract a shared helper instead of copying.

✅ Duplication (TypeScript) — clean

New TypeScript code duplication introduced by this branch. Fails at 70+ tokens in app code, or 150+ tokens when both copies live in test files. Advisory while the gate proves itself: extract a shared helper instead of copying.

@posthog posthog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PostHog Review · Standard · reviewed e5a87a4: 1 Must fix, 1 Should fix.

This is the only automatic review while this pull request is a draft. Comment @posthog review to review new changes sooner. Otherwise, a review of the new changes runs when you mark it ready for review.

See this review and its findings in PostHog Review. Comment @posthog review --deep for a Deep review. A Deep review takes longer and looks deeper than a Standard review.

@hosthog

hosthog Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

HostHog preview — posthog-desktop-web

Latest build (2ebc25f): https://63a864d90cf84f9ab06ec884d4c3136f.hosthog.dev

Employee-gated; every push gets a fresh URL whose content never changes. All previews stop serving when the PR closes.

…uests

The executor sends no body for GET and HEAD, so every signer must sign
the empty body. Only the HMAC-SHA256 signer had a receiver-side test for
that. Standard Webhooks now verifies against the bytes received for
POST, GET and HEAD, and SigV4 re-signs the received GET and HEAD request
as AWS does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@scheduled-actions-posthog

Copy link
Copy Markdown
Contributor

This PR hasn't seen activity in a week! Should it be merged, closed, or further worked on? If you want to keep it open, please remove the stale label – otherwise this will be closed in another week. If you want to permanently keep it open, use the waiting label.

Silthus and others added 3 commits November 13, 2026 04:53
A fetch that asked for Standard Webhooks and HMAC-SHA256 minted one
webhook-id per signer. The HMAC signer then overwrote the webhook-id
header that the Standard Webhooks signature covers, so receivers
rejected it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The executor must accept hmac_sha256 everywhere before any template can
ask for it, so the template choice lands in the next pr of this stack.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Silthus

Silthus commented Nov 13, 2026

Copy link
Copy Markdown
Contributor Author

Note

🤖 Claude Opus 5.5 responding on behalf of Michael

Decisions from the takeover session:

  • 👍 Deploy window Split the webhook template's signature format choice into #135206, merged only after this PR deploys in US and EU. Replaces "accept unsigned sends".
  • 🤖 Delivery id with two signers One fetch call mints one webhook-id, shared by every signer it uses.

@Silthus Silthus changed the title feat(cdp): sign webhook bodies with hmac-sha256 fix(cdp): sign the body webhooks send and accept hmac-sha256 signing Nov 13, 2026

@posthog posthog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PostHog Review · Standard · reviewed 36fd932: 1 Should fix.

Silthus and others added 3 commits November 13, 2026 06:47
SigV4 covers the headers it is given. Running it before the Standard
Webhooks or HMAC-SHA256 signer let those replace a header SigV4 had
already signed, so AWS would reject the request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Run the signed fetch calls from hog through the executor to a local
receiver, in place of the queue-payload tests. Drop the comments this
change added, inline the webhook id spread, and rename the header
override helper now that both secret headers and the HMAC signer use it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@posthog posthog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PostHog Review · Standard · reviewed 001b24e: no findings.

@Silthus
Silthus removed this pull request from stack #127119 November 13, 2026 13:23
@Silthus
Silthus added this pull request to stack #135445 November 13, 2026 13:23
Silthus and others added 2 commits November 13, 2026 13:33
Move overrideHeaders into its own module and let the Standard Webhooks
signer use it too, so its case-insensitive replace lives in one place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pass the local receiver's URL as an input, so the compiled code no
longer changes with the port, and commit the bytecode fixtures CI needs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@posthog posthog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PostHog Review · Standard · reviewed 591f9b8: no findings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@posthog posthog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PostHog Review · Standard · reviewed 9499d09: no findings.

@Silthus
Silthus marked this pull request as ready for review November 13, 2026 15:06
@parameterai

parameterai Bot commented Nov 13, 2026 •

Copy link
Copy Markdown

Risk: No findings

This increment only adds test coverage in hog-executor.service.test.ts: refusal cases for an empty-body POST and a 24-byte multi-byte secret, plus a parameterized happy path. Each assertion matches an existing fail-closed guard (empty-sentBody refusal in hog-executor-async.service.ts:500 and the Buffer.byteLength MIN_SECRET_BYTES check in hmac-sha256-signature.ts:33-35), so the change introduces no new security risk.

Sentinel reviewed 3090b4b · Review settings

@stamphog
stamphog Bot requested a review from a team November 13, 2026 15:06
@stamphog

stamphog Bot commented Nov 13, 2026

Copy link
Copy Markdown

Review requests went to the owners of the changed code. These owners were not requested because their part of this change looks trivial:

  • @PostHog/team-ingestion: Their three files are added hog bytecode fixture JSON with no assertion or behavior change, so this change doesn't need their review.

If you were listed here and want to review, you can still assign yourself as a reviewer.

@veria-ai

veria-ai Bot commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

PR overview

No security issues were found in this pull request.

Reviews

@posthog-security-review-bot posthog-security-review-bot Bot added the security-review Request a security review label Nov 13, 2026
@posthog-security-review-bot

posthog-security-review-bot Bot commented Nov 13, 2026 •

Copy link
Copy Markdown

✅ Security review complete — 9499d0911e3b

1 finding posted as a review, in 6 min.

Add the security-review label to run again on the latest commit.

@posthog-security-review-bot posthog-security-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agent-driven security review - findings inline.

@posthog-security-review-bot posthog-security-review-bot Bot removed the security-review Request a security review label Nov 13, 2026
Silthus and others added 2 commits November 13, 2026 15:22
…ecret

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-byte secret

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@posthog posthog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PostHog Review · Standard · reviewed 3090b4b: no findings.

@stamphog stamphog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not approved — this change needs a human reviewer.

Re-adding the stamphog label gives the same result unless the changed files change.

Stamphog can't approve this pull request automatically. Two gates refused it. The deny-list gate flagged it for crypto_secrets, because it changes signing code and secret handling, such as the new nodejs/src/cdp/utils/hmac-sha256-signature.ts and the edits to standard-webhooks.ts and secret-headers.ts. The tier gate classified it as T2-never (512 lines across 12 files, a fix), which is a category that always needs a human reviewer.

The size gate passed, so splitting the PR won't clear these refusals. The author should ask a human reviewer, ideally one who owns the CDP webhook and signing code, to review it.

  • posthog[bot] reviewed the current head.
Gate mechanics and policy version
Gate Result
prerequisites ✓ all clear
deny-list ✗ matches: crypto_secrets
size ✓ 190L, 7F substantive, 512L/12F incl. docs/generated/snapshots — within ceiling
tier ✗ classified as T2-never: T2-never (512L, 12F, single-area, fix)
stamphog 2.8.0 .stamphog/policy.yml @ unknown · reviewed head 3090b4b

This branch has not been deployed

No deployments
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.

2 participants