{"record":{"id":"71a5f2298a25b780","repo":"koala73/worldmonitor","slug":"invalid-returnurl-must-use-a-trusted-worldmonitor","errorCode":null,"errorMessage":"Invalid returnUrl: must use a trusted worldmonitor.app origin","messagePattern":"Invalid returnUrl: must use a trusted worldmonitor\\.app origin","errorType":"validation","errorClass":"ConvexError","httpStatus":null,"severity":"error","filePath":"convex/payments/checkout.ts","lineNumber":227,"sourceCode":"}\n\nasync function _createCheckoutSession(\n  args: CheckoutArgs,\n  user: UserInfo,\n) {\n  // Validate returnUrl to prevent open-redirect attacks.\n  const siteUrl = process.env.SITE_URL ?? \"https://worldmonitor.app\";\n  let returnUrl = siteUrl;\n  if (args.returnUrl) {\n    let parsedReturnUrl: URL;\n    try {\n      parsedReturnUrl = new URL(args.returnUrl);\n    } catch {\n      throw new ConvexError(\"Invalid returnUrl: must be a valid absolute URL\");\n    }\n\n    if (!isTrustedReturnUrlOrigin(parsedReturnUrl.origin, new URL(siteUrl).origin)) {\n      throw new ConvexError(\n        \"Invalid returnUrl: must use a trusted worldmonitor.app origin\",\n      );\n    }\n    returnUrl = parsedReturnUrl.toString();\n  }\n\n  // Build metadata: HMAC-signed userId for the webhook identity bridge.\n  const metadata: Record<string, string> = {};\n  metadata.wm_user_id = user.userId;\n  metadata.wm_user_id_sig = await signUserId(user.userId);\n  const anonymousClaimToken = ANON_ID_V4_REGEX.test(user.userId)\n    ? await signAnonClaimToken(user.userId)\n    : null;\n  if (anonymousClaimToken) {\n    metadata.wm_anon_claim = \"v2\";\n  }\n  // #6335: carry the login email that was authenticated FOR THIS CHECKOUT, so\n  // the activation webhook can address lifecycle mail without depending on the","sourceCodeStart":209,"sourceCodeEnd":245,"githubUrl":"https://github.com/koala73/worldmonitor/blob/ffec79ac339946fd2d24e85845da5755dcaa534b/convex/payments/checkout.ts#L209-L245","documentation":"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.","triggerScenarios":"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.","commonSituations":"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).","solutions":["Use one of the allowlisted origins: worldmonitor.app, www, app, api, tech, finance, commodity, happy, energy (all https)","For self-hosted/preview deployments, set the `SITE_URL` env var to your origin so it is trusted as the extraOrigin","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"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// Allowlist the returnUrl origin client-side before checkout.\nconst 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'];\nfunction trustedOrigin(u: string): boolean { try { return TRUSTED.includes(new URL(u).origin); } catch { return false; } }","typeGuard":"function isTrustedReturnUrl(value: string, trusted: readonly string[]): boolean {\n  try { return trusted.includes(new URL(value).origin); } catch { return false; }\n}","tryCatchPattern":null,"preventionTips":["Use an allowlisted worldmonitor.app origin for returnUrl","For self-hosted/preview, set SITE_URL so the deployment origin is trusted as the extraOrigin","Keep TRUSTED_RETURN_URL_ORIGINS and the pinning test in sync when adding app-serving subdomains"],"tags":["payments","checkout","security","open-redirect","url"],"backgroundTag":null,"analyzedSha":"ffec79ac339946fd2d24e85845da5755dcaa534b","analyzedAt":"2026-08-12T11:24:56.012Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}