Conversation
Update webhook signature verification with constant-time HMAC comparison and document Crove OS 2-tier hybrid architecture standard (DB mirror vs MCP protocol). Co-authored-by: Cursor <cursoragent@cursor.com>
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_9c00c95e-bf51-42eb-8807-85ff99b5c86f) |
There was a problem hiding this comment.
Code Review
This pull request updates the DOS organization synchronization webhook controller to support new organization, customer, and company events, adds safety checks for payload parsing, and introduces a new 2-Tier Hybrid Architecture standard in the documentation. Feedback on these changes highlights a potential runtime crash when passing an organization ID instead of an object to deleteTeamMember, a security risk where signature verification is bypassed if the secret is unconfigured, and a potential crash in timingSafeEqual if invalid hex characters are provided in the signature header.
| await this._orgService.deleteTeamMember( | ||
| targetOrg as any, | ||
| targetOrg.id, | ||
| targetUser.id | ||
| ); |
There was a problem hiding this comment.
The deleteTeamMember method on OrganizationService expects an Organization object as its first argument, not an organization ID string. Passing targetOrg.id will cause a runtime crash (e.g., when it tries to access org.users[0].role inside deleteTeamMember). Please pass targetOrg (or targetOrg as any if there is a type mismatch) instead.
| await this._orgService.deleteTeamMember( | |
| targetOrg as any, | |
| targetOrg.id, | |
| targetUser.id | |
| ); | |
| await this._orgService.deleteTeamMember( | |
| targetOrg as any, | |
| targetUser.id | |
| ); |
| if (!secret) { | ||
| return true; | ||
| } |
There was a problem hiding this comment.
Returning true when secret is not configured completely disables signature verification. If the environment variables are misconfigured or fail to load in production, the webhook endpoint will be left entirely unprotected, allowing unauthorized clients to trigger sync events.
Consider only bypassing signature verification in non-production environments.
| if (!secret) { | |
| return true; | |
| } | |
| if (!secret) { | |
| return process.env.NODE_ENV !== 'production'; | |
| } |
| return timingSafeEqual( | ||
| Buffer.from(cleanSignature, 'hex'), | ||
| Buffer.from(expected, 'hex') | ||
| ); |
There was a problem hiding this comment.
Using Buffer.from(cleanSignature, 'hex') can result in a buffer of a different length than Buffer.from(expected, 'hex') if cleanSignature contains any non-hex characters (since invalid characters are skipped or ignored during hex decoding). If the buffer lengths differ, crypto.timingSafeEqual will throw a TypeError: Inputs must have the same length and crash the request.
To prevent this, compare the hex strings directly as UTF-8 buffers. Since both are hex strings of equal length (verified by the length check above), their UTF-8 byte lengths are guaranteed to be identical and safe for timingSafeEqual.
| return timingSafeEqual( | |
| Buffer.from(cleanSignature, 'hex'), | |
| Buffer.from(expected, 'hex') | |
| ); | |
| return timingSafeEqual( | |
| Buffer.from(cleanSignature, 'utf-8'), | |
| Buffer.from(expected, 'utf-8') | |
| ); |
| return crypto.timingSafeEqual( | ||
| Buffer.from(cleanSignature, 'hex'), | ||
| Buffer.from(expected, 'hex') | ||
| ); |
There was a problem hiding this comment.
Using Buffer.from(cleanSignature, 'hex') can result in a buffer of a different length than Buffer.from(expected, 'hex') if cleanSignature contains any non-hex characters. If the buffer lengths differ, crypto.timingSafeEqual will throw a TypeError: Inputs must have the same length and crash the request.
To prevent this, compare the hex strings directly as UTF-8 buffers. Since both are hex strings of equal length, their UTF-8 byte lengths are guaranteed to be identical and safe for timingSafeEqual.
| return crypto.timingSafeEqual( | |
| Buffer.from(cleanSignature, 'hex'), | |
| Buffer.from(expected, 'hex') | |
| ); | |
| return crypto.timingSafeEqual( | |
| Buffer.from(cleanSignature, 'utf-8'), | |
| Buffer.from(expected, 'utf-8') | |
| ); |
What kind of change does this PR introduce?
Feature, Architecture & Webhook Standardization
Why was this change needed?
Aligns Crove Post with the Crove OS 2-Tier Hybrid Architecture Standard:
DosOrgSyncWebhookControllerto use constant-time comparisons (crypto.timingSafeEqual) and support ecosystem environment secrets (DOS_SYNC_WEBHOOK_SECRET,DOS_WEBHOOK_SECRET,JWT_SECRET).organization.created,organization.updated,customer.created,company.created).docs/architecture.md.Other information:
Checklist:
Note
Medium Risk
Changes webhook authentication and org/membership sync paths; misconfigured secrets or hex comparison could reject valid events or leave identity drift if DOS org IDs are not applied correctly.
Overview
Hardens the DOS org sync webhook to match the Crove OS verification standard and accept ecosystem event naming.
Webhook security: Signature checks now prefer
DOS_SYNC_WEBHOOK_SECRET, trim thesha256=prefix, and compare digests withtimingSafeEqualon hex-decoded buffers (not raw string bytes).Sync behavior: Handlers treat legacy
org.*and neworganization.*events the same. Payload access is guarded (data || {}, conditionalorg_idlookup). Org creation from webhooks can pass throughorg_idfor ID alignment; member-add flows setuser_namewhen provisioning users; member removal callsdeleteTeamMemberwithtargetOrg.idinstead of casting the org object.DTO & docs:
DosSyncEventaddsorganization.*pluscustomer.*/company.*values (customer/company events are not handled yet and return ignored).docs/architecture.mddocuments the 2-tier hybrid architecture and the sharedverifyEcosystemWebhookpattern.Reviewed by Cursor Bugbot for commit b79ac09. Bugbot is set up for automated code reviews on this repo. Configure here.