feat: sign outbound webhooks with hmac-sha256#197
Merged
Conversation
aa8ef37 to
13db360
Compare
13db360 to
e7ab879
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Signs outbound webhooks with an HMAC-SHA256 signature so consumers can verify authenticity. Builds on the webhook test branch (advisor/006); merge that first.
Greptile Summary
This PR adds HMAC-SHA256 signing to all outbound webhook deliveries. Each
PolydockStoreWebhooknow carries an auto-generatedsecret(hidden from JSON/API responses), and the delivery job encodes the body once, signs it, then sends exactly those bytes — ensuring the signature and the transmitted payload can never diverge.creatingboot hook mints a 40-character random secret for every new webhook, and the migration backfills one per existing row;signPayload()throws defensively if the secret is somehow absent.json_encodenow usesJSON_THROW_ON_ERRORso an encoding failure surfaces as a catchable exception rather than silently POSTing an empty body.Confidence Score: 5/5
Safe to merge — the signing logic is sound, previous feedback on empty-key and json_encode failures has been fully addressed, and the secret is correctly hidden from API responses.
All three layers of defense work together correctly: the creating hook mints a secret for new webhooks, the migration backfills existing rows with unique per-row secrets, and signPayload() throws before emitting a forged signature. The body is encoded once and reused for both the HMAC and the HTTP payload, eliminating any chance of signature/body mismatch. The new feature test verifies the end-to-end signature contract.
The migration file is the only one worth a second look — the secret column stays nullable in the schema after backfill, so the NOT NULL invariant is enforced purely at the application layer.
Important Files Changed
Sequence Diagram
%%{init: {'theme': 'neutral'}}%% sequenceDiagram participant Q as Queue participant J as ProcessPolydockStoreWebhookCall participant M as PolydockStoreWebhook participant R as Remote Endpoint Q->>J: dispatch(webhookCall) J->>J: json_encode(payload, JSON_THROW_ON_ERROR) J->>M: signPayload(body) alt secret is empty M-->>J: throw RuntimeException J->>J: catch → update status, rethrow → retry else secret present M-->>J: "sha256= + hash_hmac(sha256, body, secret)" end J->>R: "POST url / withBody(body) / X-Polydock-Signature: sha256=…" R-->>J: HTTP response alt 2xx J->>J: update status → SUCCESS else non-2xx J->>J: update status → FAILED/PENDING, throw → retry end%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%% sequenceDiagram participant Q as Queue participant J as ProcessPolydockStoreWebhookCall participant M as PolydockStoreWebhook participant R as Remote Endpoint Q->>J: dispatch(webhookCall) J->>J: json_encode(payload, JSON_THROW_ON_ERROR) J->>M: signPayload(body) alt secret is empty M-->>J: throw RuntimeException J->>J: catch → update status, rethrow → retry else secret present M-->>J: "sha256= + hash_hmac(sha256, body, secret)" end J->>R: "POST url / withBody(body) / X-Polydock-Signature: sha256=…" R-->>J: HTTP response alt 2xx J->>J: update status → SUCCESS else non-2xx J->>J: update status → FAILED/PENDING, throw → retry endComments Outside Diff (1)
app/Models/PolydockStoreWebhook.php, line 18-22 (link)secretnot in$fillableand no way to regenerate itThe auto-generated secret is set via a direct property assignment in the
creatinghook, which bypasses$fillableand works correctly on creation. However, there is no supported path to rotate a webhook secret after creation — neither via mass-assignment (blocked) nor via a dedicated method. If a secret needs to be rotated (e.g., after a suspected leak), the only option is a raw database update. Consider adding arotateSecret()method to make intentional rotation explicit and auditable, especially given theLogsActivitytrait is already in use.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Reviews (3): Last reviewed commit: "feat: sign outbound webhooks with hmac-s..." | Re-trigger Greptile