Summary
Add webhook signature verification for inbound integrations — /api/v1/webhooks/stellar and /api/v1/webhooks/soroban accept unauthenticated POST bodies and write them straight into TransactionConfirmation/dispatch, letting any external party forge transaction confirmations.
Social Media Link
Let's collaborate on Discord. And ensure to star our repo.
Problem Statement
Confirmed in corporate-platform/corporate-platform-backend/src/webhooks/webhooks.controller.ts, corporate-platform/corporate-platform-backend/src/webhooks/services/stellar-webhook.service.ts, and corporate-platform/corporate-platform-backend/src/webhooks/dto/stellar-webhook.dto.ts:
-
No guard on the delivery endpoints: registerWebhook, listWebhooks, and unregisterWebhook are protected with @UseGuards(JwtAuthGuard, RolesGuard) + @Roles('admin'), but receiveStellarWebhook (POST /stellar) and receiveSorobanEvent (POST /soroban) have no guards at all.
-
No signature header verification: neither StellarWebhookDto nor receiveStellarWebhook/receiveSorobanEvent check any X-Signature, X-Hub-Signature-256, or equivalent HMAC header before processing the payload.
-
Body is trusted and persisted directly: StellarWebhookService.registerTransaction() takes dto straight from the unauthenticated request and upserts it into transactionConfirmation, so a forged POST can create or overwrite a transaction's recorded status, ledgerSequence, and metadata.
-
receiveSorobanEvent accepts any: the DTO type is any, so the payload is dispatched to WebhookDispatcherService.dispatch() with zero schema validation in addition to zero signature check.
-
upsert allows silent overwrite of existing confirmations: a forged webhook for an existing transactionHash can flip a real pending/failed transaction to CONFIRMED (or vice versa) since registerTransaction always upserts on transactionHash with no ownership check.
-
No replay protection: even a legitimately-signed webhook, once captured, could be resent indefinitely — there is no nonce, timestamp window, or delivery-id dedup on the inbound side (distinct from WebhookDispatcherService's outbound delivery tracking).
-
No source IP or allowlist check: nothing restricts these endpoints to Stellar/Soroban infrastructure's known origins as a defense-in-depth measure.
-
No rate limiting on the public routes: receiveStellarWebhook and receiveSorobanEvent have no throttle guard, so they are also open to flooding.
-
Failure to verify has no audit trail: there is no SecurityService.logEvent call recording rejected/suspicious webhook deliveries, so an active forgery attempt would leave no security signal.
-
companyId is client-supplied and trusted: StellarWebhookDto.companyId comes directly from the unauthenticated body and is persisted onto transactionConfirmation.companyId with no verification that the transaction actually belongs to that company.
Required Changes
-
Add an HMAC-based signature verification guard (e.g. WebhookSignatureGuard) applied to receiveStellarWebhook and receiveSorobanEvent, validating a shared-secret signature header against the raw request body.
-
Store the webhook signing secret in ConfigService/environment configuration, separate from JWT_SECRET and STELLAR_SECRET_KEY.
-
Reject requests with a missing or invalid signature with 401 Unauthorized before any DTO validation or persistence occurs.
-
Replace the any type on receiveSorobanEvent's body with a validated DTO class.
-
Add a timestamp or nonce field to the expected payload and reject requests outside an acceptable time window (e.g. 5 minutes) to prevent replay.
-
Change StellarWebhookService.registerTransaction() to reject (not silently upsert) updates to a transactionHash whose existing companyId differs from the payload's companyId.
-
Derive companyId from a verified server-side mapping (e.g. registered webhook source) instead of trusting the client-supplied field where possible.
-
Add rate limiting to both public webhook routes consistent with the rest of the API's rate-limiting strategy.
-
Log rejected/invalid webhook deliveries via SecurityService.logEvent with signature-failure details (excluding the secret itself).
-
Add unit and integration tests covering: valid signature accepted, missing signature rejected, invalid signature rejected, and replayed timestamp rejected.
Acceptance Criteria
POST /api/v1/webhooks/stellar rejects requests without a valid signature header.
POST /api/v1/webhooks/soroban rejects requests without a valid signature header.
receiveSorobanEvent payloads are validated against a typed DTO, not any.
- A forged webhook payload cannot overwrite an existing
transactionConfirmation for a different company.
- Replayed (stale-timestamp) webhook deliveries are rejected.
- Rejected webhook attempts are recorded via
SecurityService.logEvent.
- Legitimate signed webhooks from the real Stellar/Soroban integration continue to be processed without behavior change.
- Rate limiting is applied to both public webhook endpoints.
- Unit tests cover valid, missing, invalid, and replayed signature scenarios.
- Webhook signing secret is documented and distinct from other application secrets.
Directory to Work on:
corporate-platform/corporate-platform-backend/
Summary
Add webhook signature verification for inbound integrations —
/api/v1/webhooks/stellarand/api/v1/webhooks/sorobanaccept unauthenticated POST bodies and write them straight intoTransactionConfirmation/dispatch, letting any external party forge transaction confirmations.Social Media Link
Let's collaborate on Discord. And ensure to star our repo.
Problem Statement
Confirmed in
corporate-platform/corporate-platform-backend/src/webhooks/webhooks.controller.ts,corporate-platform/corporate-platform-backend/src/webhooks/services/stellar-webhook.service.ts, andcorporate-platform/corporate-platform-backend/src/webhooks/dto/stellar-webhook.dto.ts:No guard on the delivery endpoints:
registerWebhook,listWebhooks, andunregisterWebhookare protected with@UseGuards(JwtAuthGuard, RolesGuard)+@Roles('admin'), butreceiveStellarWebhook(POST /stellar) andreceiveSorobanEvent(POST /soroban) have no guards at all.No signature header verification: neither
StellarWebhookDtonorreceiveStellarWebhook/receiveSorobanEventcheck anyX-Signature,X-Hub-Signature-256, or equivalent HMAC header before processing the payload.Body is trusted and persisted directly:
StellarWebhookService.registerTransaction()takesdtostraight from the unauthenticated request andupserts it intotransactionConfirmation, so a forged POST can create or overwrite a transaction's recordedstatus,ledgerSequence, andmetadata.receiveSorobanEventacceptsany: the DTO type isany, so the payload is dispatched toWebhookDispatcherService.dispatch()with zero schema validation in addition to zero signature check.upsertallows silent overwrite of existing confirmations: a forged webhook for an existingtransactionHashcan flip a real pending/failed transaction toCONFIRMED(or vice versa) sinceregisterTransactionalways upserts ontransactionHashwith no ownership check.No replay protection: even a legitimately-signed webhook, once captured, could be resent indefinitely — there is no nonce, timestamp window, or delivery-id dedup on the inbound side (distinct from
WebhookDispatcherService's outbound delivery tracking).No source IP or allowlist check: nothing restricts these endpoints to Stellar/Soroban infrastructure's known origins as a defense-in-depth measure.
No rate limiting on the public routes:
receiveStellarWebhookandreceiveSorobanEventhave no throttle guard, so they are also open to flooding.Failure to verify has no audit trail: there is no
SecurityService.logEventcall recording rejected/suspicious webhook deliveries, so an active forgery attempt would leave no security signal.companyIdis client-supplied and trusted:StellarWebhookDto.companyIdcomes directly from the unauthenticated body and is persisted ontotransactionConfirmation.companyIdwith no verification that the transaction actually belongs to that company.Required Changes
Add an HMAC-based signature verification guard (e.g.
WebhookSignatureGuard) applied toreceiveStellarWebhookandreceiveSorobanEvent, validating a shared-secret signature header against the raw request body.Store the webhook signing secret in
ConfigService/environment configuration, separate fromJWT_SECRETandSTELLAR_SECRET_KEY.Reject requests with a missing or invalid signature with
401 Unauthorizedbefore any DTO validation or persistence occurs.Replace the
anytype onreceiveSorobanEvent's body with a validated DTO class.Add a timestamp or nonce field to the expected payload and reject requests outside an acceptable time window (e.g. 5 minutes) to prevent replay.
Change
StellarWebhookService.registerTransaction()to reject (not silently upsert) updates to atransactionHashwhose existingcompanyIddiffers from the payload'scompanyId.Derive
companyIdfrom a verified server-side mapping (e.g. registered webhook source) instead of trusting the client-supplied field where possible.Add rate limiting to both public webhook routes consistent with the rest of the API's rate-limiting strategy.
Log rejected/invalid webhook deliveries via
SecurityService.logEventwith signature-failure details (excluding the secret itself).Add unit and integration tests covering: valid signature accepted, missing signature rejected, invalid signature rejected, and replayed timestamp rejected.
Acceptance Criteria
POST /api/v1/webhooks/stellarrejects requests without a valid signature header.POST /api/v1/webhooks/sorobanrejects requests without a valid signature header.receiveSorobanEventpayloads are validated against a typed DTO, notany.transactionConfirmationfor a different company.SecurityService.logEvent.Directory to Work on:
corporate-platform/corporate-platform-backend/