koala73/worldmonitor · error · ConvexError

Invalid returnUrl: must use a trusted worldmonitor.app origi

Error message

Invalid returnUrl: must use a trusted worldmonitor.app origin

What it means

Even a parseable absolute URL must have an origin in the trusted allowlist (`TRUSTED_RETURN_URL_ORIGINS`) or exactly match the deployment's `SITE_URL` origin. This is the open-redirect guard: Vercel serves the app on every attached domain, and several worldmonitor.app subdomains are vendor-owned CNAMEs, so a `*.worldmonitor.app` suffix match is deliberately avoided in favor of an enumerated list.

Source

Thrown at convex/payments/checkout.ts:227

}

async function _createCheckoutSession(
  args: CheckoutArgs,
  user: UserInfo,
) {
  // Validate returnUrl to prevent open-redirect attacks.
  const siteUrl = process.env.SITE_URL ?? "https://worldmonitor.app";
  let returnUrl = siteUrl;
  if (args.returnUrl) {
    let parsedReturnUrl: URL;
    try {
      parsedReturnUrl = new URL(args.returnUrl);
    } catch {
      throw new ConvexError("Invalid returnUrl: must be a valid absolute URL");
    }

    if (!isTrustedReturnUrlOrigin(parsedReturnUrl.origin, new URL(siteUrl).origin)) {
      throw new ConvexError(
        "Invalid returnUrl: must use a trusted worldmonitor.app origin",
      );
    }
    returnUrl = parsedReturnUrl.toString();
  }

  // Build metadata: HMAC-signed userId for the webhook identity bridge.
  const metadata: Record<string, string> = {};
  metadata.wm_user_id = user.userId;
  metadata.wm_user_id_sig = await signUserId(user.userId);
  const anonymousClaimToken = ANON_ID_V4_REGEX.test(user.userId)
    ? await signAnonClaimToken(user.userId)
    : null;
  if (anonymousClaimToken) {
    metadata.wm_anon_claim = "v2";
  }
  // #6335: carry the login email that was authenticated FOR THIS CHECKOUT, so
  // the activation webhook can address lifecycle mail without depending on the

View on GitHub (pinned to ffec79ac33)

Solutions

  1. Use one of the allowlisted origins: worldmonitor.app, www, app, api, tech, finance, commodity, happy, energy (all https)
  2. For self-hosted/preview deployments, set the `SITE_URL` env var to your origin so it is trusted as the extraOrigin
  3. If a legitimate new app-serving subdomain is missing, add it to TRUSTED_RETURN_URL_ORIGINS and update tests/checkout-return-url-origin.test.mts
Defensive patterns

Strategy: validation

Validate before calling

// Allowlist the returnUrl origin client-side before checkout.
const TRUSTED = ['https://worldmonitor.app','https://www.worldmonitor.app','https://app.worldmonitor.app','https://api.worldmonitor.app','https://tech.worldmonitor.app','https://finance.worldmonitor.app','https://commodity.worldmonitor.app','https://happy.worldmonitor.app','https://energy.worldmonitor.app'];
function trustedOrigin(u: string): boolean { try { return TRUSTED.includes(new URL(u).origin); } catch { return false; } }

Type guard

function isTrustedReturnUrl(value: string, trusted: readonly string[]): boolean {
  try { return trusted.includes(new URL(value).origin); } catch { return false; }
}

Prevention

When it happens

Trigger: Passing a `returnUrl` whose origin is not in the allowlist: e.g. `https://evil.com/...`, a non-listed subdomain like `https://staging.worldmonitor.app`, or an `http://` variant of an allowed host.

Common situations: Trying to redirect post-payment to a staging/preview domain not in the list; third-party URL; http vs https mismatch; a new variant subdomain added to Vercel but not to the allowlist (the documented drift incident WORLDMONITOR-K7).

Related errors


AI-assisted analysis of koala73/worldmonitor@ffec79ac33 (2026-08-12). Data as JSON: /api/errors/71a5f2298a25b780. Report an issue: GitHub.