Titan
PricingPentest
Log in
All Advisories
TITAN-2026-002Medium

Open Redirect in Stripe Payment and Feishu Calendar OAuth Callbacks

User-controlled redirect URLs from the OAuth state parameter are passed directly to res.redirect() without getSafeRedirectUrl() validation in Stripe Payment and Feishu Calendar callback handlers, allowing redirection to arbitrary external domains.

CVSS Score
6.1 / 10.0
Product
Cal.com
Vendor
Cal.com, Inc. (calcom/cal.com)
Affected Versions
< v6.2.0
Fixed In
v6.2.0 (commit 14c151be)
Published
2026-03-04
Updated
2026-03-04
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N

Description

The Stripe Payment OAuth callback handler at packages/app-store/stripepayment/api/callback.ts has two unvalidated redirect paths. At line 29, state.onErrorReturnTo is used directly in res.redirect() when a user cancels the OAuth flow (error === "access_denied"). At lines 57-58, the getReturnToValueFromQueryState() helper extracts returnTo from req.query.state JSON without URL validation and passes it directly to res.redirect().

The Feishu Calendar OAuth callback at packages/app-store/feishucalendar/api/callback.ts correctly uses getSafeRedirectUrl() on the success path (lines 119-122), but the catch block at line 125 bypasses it entirely, redirecting to state?.returnTo without validation. Triggering the error path is straightforward — providing an invalid or expired authorization code causes the token exchange to fail.


The identical larkcalendar/api/callback.ts was previously patched for this same issue, but the fix was never propagated to feishucalendar.

Technical Details

Stripe Payment — Line 29 (`onErrorReturnTo`):

if (error === "access_denied") {
  state?.onErrorReturnTo
    ? res.redirect(state.onErrorReturnTo)           // no validation
    : res.redirect("/apps/installed/payment");
}



Stripe Payment — Lines 12-19, 57-58 (`returnTo`):

function getReturnToValueFromQueryState(req: NextApiRequest) {
  let returnTo = "";
  try {
    returnTo = JSON.parse(`${req.query.state}`).returnTo;
  } catch (error) {
    console.info("No 'returnTo' in req.query.state");
  }
  return returnTo;
}

const returnTo = getReturnToValueFromQueryState(req);
res.redirect(returnTo || getInstalledAppPath({ variant: "payment", slug: "stripe" }));



Feishu Calendar — Line 125 (catch block):

// line 119 — safe:
res.redirect(getSafeRedirectUrl(state?.returnTo) ?? getInstalledAppPath({ variant: "calendar", slug: "feishu-calendar" }));

// line 125 — vulnerable:
} catch (error) {
  log.error("handle callback error", error);
  res.redirect(state?.returnTo ?? "/apps/installed");
}

Remediation

Apply the existing getSafeRedirectUrl() utility to all three unvalidated redirect locations, consistent with 20+ other OAuth callbacks in the codebase.

Stripe Payment — Line 29:

const redirectUrl = getSafeRedirectUrl(state?.onErrorReturnTo) ?? "/apps/installed/payment";
res.redirect(redirectUrl);



Stripe Payment — Line 58:

const returnTo = getReturnToValueFromQueryState(req);
res.redirect(getSafeRedirectUrl(returnTo) ?? getInstalledAppPath({ variant: "payment", slug: "stripe" }));



Feishu Calendar — Line 125:

res.redirect(getSafeRedirectUrl(state?.returnTo) ?? "/apps/installed");

Timeline

2026-02-12Reported to Cal.com Security over email
2026-02-20Fixed by Cal.com team in commit 14c151be
2026-03-02v6.2.0 released with fix
2026-03-04Advisory published

References

  • Cal.com GitHub Repository
  • Fix Commit (14c151be)
  • CWE-601: URL Redirection to Untrusted Site

Credit

Ananay Arora at Titan Security Research

AI-powered application security that finds real vulnerabilities.

Product

  • Security Agent
  • PR Integration
  • AI Autofix
  • Custom Context
  • Pricing

Services

  • Managed Pentesting

Solutions

  • Application Security
  • DevSecOps
  • Compliance
  • For Security Engineers
  • For Developers
  • For CISOs

Company

  • About
  • Wall of Fame
  • Blog
  • Contact

Legal

  • Privacy Policy
  • Terms of Service

© 2026 Titan Security Labs, Inc. All rights reserved.

PrivacyTerms[email protected]