All Advisories
mediumCVE PendingTitan-2026-002OH-2026-002

Open Redirect in Stripe Payment and Feishu Calendar OAuth Callbacks

Product
Cal.com
Vendor
Cal.com, Inc. (calcom/cal.com)
Affected
< v6.2.0
Fixed in
v6.2.0 (commit 14c151be)
CVSS
6.1
Published
2026-03-04

CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N

Summary

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.

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`)

typescript 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`)

typescript 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)

```typescript // 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

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

Stripe Payment — Line 58

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

Feishu Calendar — Line 125

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

Timeline

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

References

Credit: Ananay Arora at OpenHack Security Research