Skip to content

Validate and sandbox merchant webhook URLs to prevent SSRF from webhook delivery #441

Description

@Cedarich
  • Complexity: Hard
  • Labels: Backend, Hard, security, webhooks, validation
  • Overview: Merchants control their own webhook destination, and the backend POSTs to that URL with no validation of the scheme, host, or resolved IP. UpdateMerchantSettingsDto declares webhookUrl with only @IsString() and @IsOptional() — there is no @IsUrl() and no allowlist. WebhooksService then calls axios.post(merchant.webhookUrl, ...) and axios.post(delivery.url, ...) directly. Any authenticated merchant can point the webhook at internal infrastructure and make the backend issue requests on their behalf, which is a server-side request forgery primitive.
  • Details:
    • A merchant can set webhookUrl to a loopback address (http://127.0.0.1:3001/...), a private range (http://10.0.0.5/...), a link-local metadata endpoint (http://169.254.169.254/latest/meta-data/), or a non-HTTP scheme, and the backend will call it.
    • The test-delivery endpoint returns the HTTP status and error classification to the caller, and delivery history records lastHttpStatus. That turns the SSRF into an oracle: a merchant can probe internal hosts and ports and read the outcome back through the API.
    • axios follows redirects by default. Even if the configured URL is validated at write time, a public endpoint can 302 the backend to an internal address at delivery time, so validation must also hold at request time.
    • DNS is resolved at request time, so a hostname that validates as public on write can later resolve to a private address (DNS rebinding). Write-time validation alone is not sufficient.
  • Scope:
    • Validate webhookUrl on write: require a valid absolute URL, restrict to https (with an explicit, configurable escape hatch for local development), and reject credentials embedded in the URL.
    • Block requests to loopback, private, link-local, unique-local, and reserved address ranges, enforced against the resolved IP at request time rather than only the hostname.
    • Disable or tightly bound redirect following on webhook delivery, and re-validate the destination on every hop that is allowed.
    • Ensure the test-delivery and delivery-history responses do not leak internal reachability details beyond what a merchant needs to debug their own endpoint.
    • Keep an explicit configuration switch so local and CI environments can still target localhost deliberately.
  • Technical scope:
    • backend/src/merchant/dto/update-merchant-settings.dto.ts
    • backend/src/merchant/merchant.service.ts
    • backend/src/webhooks/webhooks.service.ts
    • backend/src/webhooks/webhooks.controller.ts
    • backend/src/config/app.config.ts
  • Acceptance criteria:
    • Saving a webhook URL that points at a loopback, private, link-local, or reserved address is rejected with a clear validation error.
    • Non-HTTP(S) schemes and URLs containing embedded credentials are rejected.
    • A webhook endpoint that redirects to an internal address does not result in a request to that address.
    • A hostname that resolves to a private address at delivery time is blocked even if it validated at write time.
    • Local development can still target localhost via explicit configuration, and that configuration is off by default.
    • Tests cover each blocked address class, the redirect case, the rebinding case, and the allowed public case.

Activity

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

Metadata

Metadata

Assignees

Labels

BackendGrantFox OSSIssue tracked in GrantFox OSSHardHigh-complexity taskMaybe RewardedIssue may be eligible for a GrantFox rewardThird CampaignCampaign: Third CampaignsecuritySecurity-related workvalidationValidation and input handlingwebhooksWebhook delivery and management

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions