fix(extension): region-aware URLs to prevent cross-region data leak#93
Draft
dhairyashiil wants to merge 7 commits into
Draft
fix(extension): region-aware URLs to prevent cross-region data leak#93dhairyashiil wants to merge 7 commits into
dhairyashiil wants to merge 7 commits into
Conversation
The hook ran 'cd companion' from a non-existent subdirectory — a leftover from when this code lived inside calcom/cal as a 'companion/' subdir. Replace with 'bunx lint-staged' run from the repo root so commits aren't blocked in fresh clones.
Replace 13 hardcoded https://cal.com / https://app.cal.com URLs in content scripts (Gmail integration, embed slot picker, fallback handles) with the getCalAppUrl() / getCalWebUrl() helpers from lib/region. EU users will now get cal.eu / app.cal.eu URLs in composed emails, copied booking links, and 'Edit event type' deep links.
LinkedIn's 'Edit event type' and the booking-URL fallback both pointed at https://app.cal.com / https://cal.com. Route them through the new getCalAppUrl() / getCalWebUrl() helpers so EU hosts get app.cal.eu / cal.eu links.
extractBookingUid()'s regex matched only https://(app.)?cal.com/booking/{uid},
so descriptions referencing cal.eu booking URLs never triggered the
no-show button injection. Broaden the regex to (com|eu) and switch the
two text.includes() host checks to a lowercased CAL_WEB_HOSTNAMES scan
so cal.com, Cal.com, cal.eu, Cal.eu, app.cal.com, app.cal.eu all match.
openAppPage() now routes through getCalMarketingAppUrl() so the policy (Framer marketing is intentionally cross-region) is explicit at the helper level rather than inline. apiBaseUrlForRegion() is deleted; both call sites (getApiBaseUrl and validateTokens) now build the URL via getCalApiUrl(region) + '/v2', so there is a single source of truth for Cal.com origins shared with the content-script callsites.
The CI grep previously scanned only apps/mobile, so the extension drifted without enforcement. After this change: - cd walks up to repo root (was: apps/mobile only) - rg scans both apps/mobile AND apps/extension - PATTERN broadens to \b(app\.)?cal\.(com|eu)\b so app.cal.eu also fails - FILE_ALLOWLIST is path-anchored (^path:) so future code lines can't bypass the check by mentioning a hostname; adds extension paths that must stay literal (lib/region.ts, wxt.config.ts, public/manifest.json) - CONTENT_ALLOWLIST exempts the companion.cal.com OAuth/iframe origin, which is not a Cal app URL Verified manually: planting https://cal.com/foo or https://app.cal.eu/foo in extension code makes the script exit 1; reverting brings it back to exit 0.
Contributor
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
|
Deployment failed with the following error: View Documentation: https://vercel.com/docs/accounts/team-members-and-roles |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The Chrome extension's content scripts and background worker emitted client-side Cal.com URLs (Gmail booking links, LinkedIn integration links, "Edit event type" deep links, the "open Cal.com" tab, and the API base) that were hardcoded to
cal.com/app.cal.comregardless of the user's data region. An EU host (whose data lives on cal.eu) composing a Gmail email would attach ahttps://cal.com/<their-username>/<slot>link to attendees — best case the recipient sees a 404; worst case the same username exists on cal.com (independent identity realm — a different person) and the recipient books with the wrong person, exfiltrating the EU host's calendar metadata.The server-side fix in calcom/cal#2808 does NOT cover this — these URLs are emitted client-side from extension code, never validated against a region-bound JWT. The CI grep
check:no-cal-hostnames.shscanned onlyapps/mobile, so the extension drifted without enforcement.Changes
apps/extension/lib/region.ts— mirrorsapps/mobile/utils/region.ts. Readschrome.storage.local["cal_region"](the same key the background worker writes), caches synchronously, and refreshes viachrome.storage.onChanged. ExportsgetCalAppUrl,getCalApiUrl,getCalWebUrl,getCalMarketingAppUrl,CAL_WEB_HOSTNAMES, pluspreloadRegion/getRegion.entrypoints/content.ts— 13 sites (Gmail menu/embed/slot-picker links, edit/create event-type deep-links, fallback booking handles)lib/linkedin.ts— 2 sites (edit event-type deep-link, booking URL fallback)lib/google-calendar.ts— 3 sites: regex broadened to matchcal.{com|eu}/booking/<uid>; twotext.includes("cal.com") || text.includes("Cal.com")host checks replaced with a lowercasedCAL_WEB_HOSTNAMES.some(...)scan socal.com,Cal.com,cal.eu,Cal.eu,app.cal.com,app.cal.euall trigger no-show button injectionentrypoints/background/index.ts—openAppPage()routes throughgetCalMarketingAppUrl()(Framer marketing is intentionally cross-region, same precedent as mobile'sgetCalSupportUrl());apiBaseUrlForRegion()deleted, both call sites now use${getCalApiUrl(region)}/v2for one source of truthapps/mobileandapps/extension. File-path-anchored allowlist (was content-anchored) so future code lines can't bypass by mentioning a hostname. Pattern broadened to also flagapp.cal.{com|eu}.companion.cal.com(OAuth/iframe origin — different surface) stays content-allowlisted..husky/pre-commit— fixed a pre-existing broken hook (cd companionreferencing a non-existent subdir, leftover from when this code lived insidecalcom/cal). Replaced withbunx lint-stagedrun from the repo root. This was blocking all commits in fresh clones.Verified locally
bun run typecheck— all 4 workspaces (extension, mobile, mcp-server, chat) cleanbun run lint:all— biome + react-compiler eslint + the extendedcheck:no-cal-hostnames.sh— exit 0https://cal.com/fooorhttps://app.cal.eu/footo extension code makes the script exit 1; reverting brings it back to exit 0Review & Testing Checklist for Human
cd apps/extension && bun dev), load unpacked, then in the service worker DevTools console runawait chrome.storage.local.set({ cal_region: "eu" }). Verify Gmail slot-picker links point tocal.eu, LinkedIn "Edit event type" points toapp.cal.eu, and Google Calendar events whose description containshttps://cal.eu/booking/abc123still get the no-show button. Switch back tousand confirm cal.com URLs return without an extension reload (thechrome.storage.onChangedlistener should refresh the cache).getCalWebUrl()after extension load can return the US default if it lands before the fire-and-forgetpreloadRegion()resolves. In practice content scripts only call these helpers in user-event handlers, but worth eyeballing on a slow-starting Gmail tab.getApiBaseUrl()still refreshes per call — keptawait getStoredRegion()in the path rather than reading the cached region fromlib/region.ts, since the service worker can be suspended/woken and we want a fresh storage read per request. Confirm by toggling region and immediately triggering a background API call.apiBaseUrlForRegionremoval. Two call sites updated (getApiBaseUrlandvalidateTokens); double-check no other callers I missed.wxt.config.ts,public/manifest.json,apps/extension/.env.example) are genuinely intentional — they declare manifesthost_permissions/ privacy URL that must be static at build time.Notes
vitesttoapps/extension— the parallel mobileutils/region.tsmodule has no unit tests either, so adding test infra only on the extension side would be inconsistent. Validation is via the smoke test + the CI grep regression test above.<all_urls>content-script scope reduction (separate bug, separate PR)..husky/pre-commitfix is bundled here because it was blocking my own commits. Happy to split it into a separate PR if preferred.Link to Devin session: https://app.devin.ai/sessions/ac67e96833434118859e913e2ed01489
Requested by: @dhairyashiil