All Docs
FeaturesCalmony PayUpdated March 15, 2026

Security: SSRF Prevention for Checkout Redirect URLs

Security: SSRF Prevention for Checkout Redirect URLs

Release: v1.0.27 · Control: SEC-10 · Category: OWASP

Background

The POST /v1/checkout/sessions endpoint accepts two caller-supplied redirect URLs:

FieldPurpose
success_urlWhere the customer is sent after a successful payment
cancel_urlWhere the customer is sent if they abandon the checkout

Prior to v1.0.27, these fields were validated only with z.string().url(), which confirms URL syntax but places no restriction on the destination host.

The Risk

z.string().url() accepts addresses such as:

http://localhost/admin
http://127.0.0.1:8080/internal
http://169.254.169.254/latest/meta-data/   # AWS/GCP instance metadata
http://10.0.0.1/
http://192.168.1.1/

Both URLs are stored in the database at session creation time. The immediate code path uses NextResponse.redirect() — a browser-level 303 redirect — so no outbound server request is made today. However, the stored URLs represent a latent Server-Side Request Forgery (SSRF) risk: if any future server-side logic consumes these URLs (e.g. posting a webhook notification to the merchant's success_url), an attacker could direct internal requests to the instance metadata service or private network infrastructure.

Fix Applied

A isSafeUrl() validation helper is now enforced on both success_url and cancel_url before the checkout session is persisted.

Validation rules

A URL is considered safe only when all of the following are true:

  1. Protocol is https: — plain HTTP URLs are rejected.
  2. Hostname is not localhost — the literal string localhost is blocked.
  3. Hostname is not a loopback address127.0.0.0/8 is blocked.
  4. Hostname is not a link-local address169.254.0.0/16 is blocked (covers the AWS EC2 and GCP instance metadata endpoints).
  5. Hostname is not an RFC-1918 private range10.0.0.0/8, 172.16.0.0/12, and 192.168.0.0/16 are all blocked.

Helper signature

function isSafeUrl(url: string): boolean {
  const u = new URL(url);
  return u.protocol === 'https:' && !isInternalHost(u.hostname);
}

If either success_url or cancel_url fails this check, the request is rejected with an HTTP 400 response before any database write occurs.

Impact on Existing Integrations

ScenarioBehaviour
success_url is a public https:// domain✅ No change
success_url uses http://❌ Rejected — upgrade to https://
Either URL points to a private or loopback address❌ Rejected
Either URL points to the instance metadata endpoint❌ Rejected

Action required for local development: If you use http://localhost redirect URLs in a local or staging environment, update your test sessions to use a tunnelled https:// URL (e.g. from ngrok or Cloudflare Tunnel) or configure an environment-specific bypass for non-production builds.

Affected Endpoint

POST /v1/checkout/sessions

Fields validated: success_url, cancel_url

File: src/app/api/v1/checkout/sessions/route.ts

References