Skip to content

Add webhook signature verification for inbound integrations #544

Description

@Oluwaseyi89

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:

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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).

  7. No source IP or allowlist check: nothing restricts these endpoints to Stellar/Soroban infrastructure's known origins as a defense-in-depth measure.

  8. No rate limiting on the public routes: receiveStellarWebhook and receiveSorobanEvent have no throttle guard, so they are also open to flooding.

  9. 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.

  10. 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

  1. 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.

  2. Store the webhook signing secret in ConfigService/environment configuration, separate from JWT_SECRET and STELLAR_SECRET_KEY.

  3. Reject requests with a missing or invalid signature with 401 Unauthorized before any DTO validation or persistence occurs.

  4. Replace the any type on receiveSorobanEvent's body with a validated DTO class.

  5. 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.

  6. Change StellarWebhookService.registerTransaction() to reject (not silently upsert) updates to a transactionHash whose existing companyId differs from the payload's companyId.

  7. Derive companyId from a verified server-side mapping (e.g. registered webhook source) instead of trusting the client-supplied field where possible.

  8. Add rate limiting to both public webhook routes consistent with the rest of the API's rate-limiting strategy.

  9. Log rejected/invalid webhook deliveries via SecurityService.logEvent with signature-failure details (excluding the secret itself).

  10. Add unit and integration tests covering: valid signature accepted, missing signature rejected, invalid signature rejected, and replayed timestamp rejected.

Acceptance Criteria

  1. POST /api/v1/webhooks/stellar rejects requests without a valid signature header.
  2. POST /api/v1/webhooks/soroban rejects requests without a valid signature header.
  3. receiveSorobanEvent payloads are validated against a typed DTO, not any.
  4. A forged webhook payload cannot overwrite an existing transactionConfirmation for a different company.
  5. Replayed (stale-timestamp) webhook deliveries are rejected.
  6. Rejected webhook attempts are recorded via SecurityService.logEvent.
  7. Legitimate signed webhooks from the real Stellar/Soroban integration continue to be processed without behavior change.
  8. Rate limiting is applied to both public webhook endpoints.
  9. Unit tests cover valid, missing, invalid, and replayed signature scenarios.
  10. Webhook signing secret is documented and distinct from other application secrets.

Directory to Work on:

corporate-platform/corporate-platform-backend/

Activity

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

Metadata

Metadata

Labels

Nest.jsThis issue is to be implemented with Nest.js frameworkStellar WaveIssues in the Stellar wave programTypescriptThis issue is to be implemented with TypescriptbackendThis issue is about building backend API services.

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions