From 491bd61610e956d2d29f7b66e3a32afbdec2ba33 Mon Sep 17 00:00:00 2001 From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com> Date: Tue, 6 Jan 2026 22:26:12 +0200 Subject: [PATCH 1/5] security: add Secure attribute to cookies --- .../[emailAccountId]/calendars/ConnectCalendar.tsx | 2 +- apps/web/app/utm.tsx | 12 ++++++------ apps/web/components/ui/sidebar.tsx | 2 +- apps/web/utils/auth-cookies.ts | 2 +- apps/web/utils/cookies.ts | 6 +++--- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/apps/web/app/(app)/[emailAccountId]/calendars/ConnectCalendar.tsx b/apps/web/app/(app)/[emailAccountId]/calendars/ConnectCalendar.tsx index d0ee2f0f2..4430c624c 100644 --- a/apps/web/app/(app)/[emailAccountId]/calendars/ConnectCalendar.tsx +++ b/apps/web/app/(app)/[emailAccountId]/calendars/ConnectCalendar.tsx @@ -22,7 +22,7 @@ export function ConnectCalendar({ const setOnboardingReturnCookie = () => { if (onboardingReturnPath) { - document.cookie = `${CALENDAR_ONBOARDING_RETURN_COOKIE}=${encodeURIComponent(onboardingReturnPath)}; path=/; max-age=180`; + document.cookie = `${CALENDAR_ONBOARDING_RETURN_COOKIE}=${encodeURIComponent(onboardingReturnPath)}; path=/; max-age=180; SameSite=Lax; Secure`; } }; diff --git a/apps/web/app/utm.tsx b/apps/web/app/utm.tsx index a848d31bd..30f842692 100644 --- a/apps/web/app/utm.tsx +++ b/apps/web/app/utm.tsx @@ -15,17 +15,17 @@ function setUtmCookies() { const expires = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toUTCString(); if (utmSource) - document.cookie = `utm_source=${utmSource}; expires=${expires}; path=/`; + document.cookie = `utm_source=${utmSource}; expires=${expires}; path=/; SameSite=Lax; Secure`; if (utmMedium) - document.cookie = `utm_medium=${utmMedium}; expires=${expires}; path=/`; + document.cookie = `utm_medium=${utmMedium}; expires=${expires}; path=/; SameSite=Lax; Secure`; if (utmCampaign) - document.cookie = `utm_campaign=${utmCampaign}; expires=${expires}; path=/`; + document.cookie = `utm_campaign=${utmCampaign}; expires=${expires}; path=/; SameSite=Lax; Secure`; if (utmTerm) - document.cookie = `utm_term=${utmTerm}; expires=${expires}; path=/`; + document.cookie = `utm_term=${utmTerm}; expires=${expires}; path=/; SameSite=Lax; Secure`; if (affiliate) - document.cookie = `affiliate=${affiliate}; expires=${expires}; path=/`; + document.cookie = `affiliate=${affiliate}; expires=${expires}; path=/; SameSite=Lax; Secure`; if (referralCode) - document.cookie = `referral_code=${referralCode}; expires=${expires}; path=/`; + document.cookie = `referral_code=${referralCode}; expires=${expires}; path=/; SameSite=Lax; Secure`; } export function UTM() { diff --git a/apps/web/components/ui/sidebar.tsx b/apps/web/components/ui/sidebar.tsx index 01f391b70..f8c0c5e16 100644 --- a/apps/web/components/ui/sidebar.tsx +++ b/apps/web/components/ui/sidebar.tsx @@ -90,7 +90,7 @@ const SidebarProvider = React.forwardRef< // This sets the cookie to keep the sidebar state. // This sets the cookie to keep the sidebar state. sidebarNames.forEach((sidebarName) => { - document.cookie = `${sidebarName}:state=${openState.includes(sidebarName)}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`; + document.cookie = `${sidebarName}:state=${openState.includes(sidebarName)}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}; SameSite=Lax; Secure`; }); }, [setOpenProp, open, sidebarNames], diff --git a/apps/web/utils/auth-cookies.ts b/apps/web/utils/auth-cookies.ts index f60e11db9..e0345e4f5 100644 --- a/apps/web/utils/auth-cookies.ts +++ b/apps/web/utils/auth-cookies.ts @@ -7,7 +7,7 @@ export function getAndClearAuthErrorCookie(): string | undefined { .join("="); if (authErrorCookie) { - document.cookie = "auth_error=; path=/; max-age=0"; + document.cookie = "auth_error=; path=/; max-age=0; SameSite=Lax; Secure"; } return authErrorCookie; diff --git a/apps/web/utils/cookies.ts b/apps/web/utils/cookies.ts index d7f604b57..82f5088d6 100644 --- a/apps/web/utils/cookies.ts +++ b/apps/web/utils/cookies.ts @@ -9,13 +9,13 @@ export type LastEmailAccountCookieValue = { }; export function markOnboardingAsCompleted(cookie: string) { - document.cookie = `${cookie}=true; path=/; max-age=${Number.MAX_SAFE_INTEGER}; SameSite=Lax`; + document.cookie = `${cookie}=true; path=/; max-age=${Number.MAX_SAFE_INTEGER}; SameSite=Lax; Secure`; } export function setInvitationCookie(invitationId: string) { - document.cookie = `${INVITATION_COOKIE}=${invitationId}; path=/; max-age=${7 * 24 * 60 * 60}; SameSite=Lax`; + document.cookie = `${INVITATION_COOKIE}=${invitationId}; path=/; max-age=${7 * 24 * 60 * 60}; SameSite=Lax; Secure`; } export function clearInvitationCookie() { - document.cookie = `${INVITATION_COOKIE}=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax`; + document.cookie = `${INVITATION_COOKIE}=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax; Secure`; } From 5a4be536be7b34290a8022977d9cf8e8a7c575b5 Mon Sep 17 00:00:00 2001 From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com> Date: Tue, 6 Jan 2026 22:49:23 +0200 Subject: [PATCH 2/5] fix --- apps/web/app/utm.tsx | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/apps/web/app/utm.tsx b/apps/web/app/utm.tsx index 30f842692..53ac33bac 100644 --- a/apps/web/app/utm.tsx +++ b/apps/web/app/utm.tsx @@ -13,19 +13,22 @@ function setUtmCookies() { // expires in 30 days const expires = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toUTCString(); + const isSecureContext = + typeof window !== "undefined" && window.location.protocol === "https:"; + const secureAttr = isSecureContext ? "; Secure" : ""; if (utmSource) - document.cookie = `utm_source=${utmSource}; expires=${expires}; path=/; SameSite=Lax; Secure`; + document.cookie = `utm_source=${encodeURIComponent(utmSource)}; expires=${expires}; path=/; SameSite=Lax${secureAttr}`; if (utmMedium) - document.cookie = `utm_medium=${utmMedium}; expires=${expires}; path=/; SameSite=Lax; Secure`; + document.cookie = `utm_medium=${encodeURIComponent(utmMedium)}; expires=${expires}; path=/; SameSite=Lax${secureAttr}`; if (utmCampaign) - document.cookie = `utm_campaign=${utmCampaign}; expires=${expires}; path=/; SameSite=Lax; Secure`; + document.cookie = `utm_campaign=${encodeURIComponent(utmCampaign)}; expires=${expires}; path=/; SameSite=Lax${secureAttr}`; if (utmTerm) - document.cookie = `utm_term=${utmTerm}; expires=${expires}; path=/; SameSite=Lax; Secure`; + document.cookie = `utm_term=${encodeURIComponent(utmTerm)}; expires=${expires}; path=/; SameSite=Lax${secureAttr}`; if (affiliate) - document.cookie = `affiliate=${affiliate}; expires=${expires}; path=/; SameSite=Lax; Secure`; + document.cookie = `affiliate=${encodeURIComponent(affiliate)}; expires=${expires}; path=/; SameSite=Lax${secureAttr}`; if (referralCode) - document.cookie = `referral_code=${referralCode}; expires=${expires}; path=/; SameSite=Lax; Secure`; + document.cookie = `referral_code=${encodeURIComponent(referralCode)}; expires=${expires}; path=/; SameSite=Lax${secureAttr}`; } export function UTM() { From c25a7b6d0c7c45eb9b2f3c72d1a7ee1c6f4a87bc Mon Sep 17 00:00:00 2001 From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com> Date: Tue, 6 Jan 2026 22:51:01 +0200 Subject: [PATCH 3/5] security: encode UTM cookie values to prevent injection --- apps/web/app/utm.tsx | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/apps/web/app/utm.tsx b/apps/web/app/utm.tsx index 53ac33bac..0e2adda8c 100644 --- a/apps/web/app/utm.tsx +++ b/apps/web/app/utm.tsx @@ -13,22 +13,19 @@ function setUtmCookies() { // expires in 30 days const expires = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toUTCString(); - const isSecureContext = - typeof window !== "undefined" && window.location.protocol === "https:"; - const secureAttr = isSecureContext ? "; Secure" : ""; if (utmSource) - document.cookie = `utm_source=${encodeURIComponent(utmSource)}; expires=${expires}; path=/; SameSite=Lax${secureAttr}`; + document.cookie = `utm_source=${encodeURIComponent(utmSource)}; expires=${expires}; path=/; SameSite=Lax; Secure`; if (utmMedium) - document.cookie = `utm_medium=${encodeURIComponent(utmMedium)}; expires=${expires}; path=/; SameSite=Lax${secureAttr}`; + document.cookie = `utm_medium=${encodeURIComponent(utmMedium)}; expires=${expires}; path=/; SameSite=Lax; Secure`; if (utmCampaign) - document.cookie = `utm_campaign=${encodeURIComponent(utmCampaign)}; expires=${expires}; path=/; SameSite=Lax${secureAttr}`; + document.cookie = `utm_campaign=${encodeURIComponent(utmCampaign)}; expires=${expires}; path=/; SameSite=Lax; Secure`; if (utmTerm) - document.cookie = `utm_term=${encodeURIComponent(utmTerm)}; expires=${expires}; path=/; SameSite=Lax${secureAttr}`; + document.cookie = `utm_term=${encodeURIComponent(utmTerm)}; expires=${expires}; path=/; SameSite=Lax; Secure`; if (affiliate) - document.cookie = `affiliate=${encodeURIComponent(affiliate)}; expires=${expires}; path=/; SameSite=Lax${secureAttr}`; + document.cookie = `affiliate=${encodeURIComponent(affiliate)}; expires=${expires}; path=/; SameSite=Lax; Secure`; if (referralCode) - document.cookie = `referral_code=${encodeURIComponent(referralCode)}; expires=${expires}; path=/; SameSite=Lax${secureAttr}`; + document.cookie = `referral_code=${encodeURIComponent(referralCode)}; expires=${expires}; path=/; SameSite=Lax; Secure`; } export function UTM() { From a9908ef0806e615a23a09f3545753df676185266 Mon Sep 17 00:00:00 2001 From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com> Date: Tue, 6 Jan 2026 22:59:50 +0200 Subject: [PATCH 4/5] revert: remove encodeURIComponent from UTM cookies (readers don't decode) --- apps/web/app/utm.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/web/app/utm.tsx b/apps/web/app/utm.tsx index 0e2adda8c..30f842692 100644 --- a/apps/web/app/utm.tsx +++ b/apps/web/app/utm.tsx @@ -15,17 +15,17 @@ function setUtmCookies() { const expires = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toUTCString(); if (utmSource) - document.cookie = `utm_source=${encodeURIComponent(utmSource)}; expires=${expires}; path=/; SameSite=Lax; Secure`; + document.cookie = `utm_source=${utmSource}; expires=${expires}; path=/; SameSite=Lax; Secure`; if (utmMedium) - document.cookie = `utm_medium=${encodeURIComponent(utmMedium)}; expires=${expires}; path=/; SameSite=Lax; Secure`; + document.cookie = `utm_medium=${utmMedium}; expires=${expires}; path=/; SameSite=Lax; Secure`; if (utmCampaign) - document.cookie = `utm_campaign=${encodeURIComponent(utmCampaign)}; expires=${expires}; path=/; SameSite=Lax; Secure`; + document.cookie = `utm_campaign=${utmCampaign}; expires=${expires}; path=/; SameSite=Lax; Secure`; if (utmTerm) - document.cookie = `utm_term=${encodeURIComponent(utmTerm)}; expires=${expires}; path=/; SameSite=Lax; Secure`; + document.cookie = `utm_term=${utmTerm}; expires=${expires}; path=/; SameSite=Lax; Secure`; if (affiliate) - document.cookie = `affiliate=${encodeURIComponent(affiliate)}; expires=${expires}; path=/; SameSite=Lax; Secure`; + document.cookie = `affiliate=${affiliate}; expires=${expires}; path=/; SameSite=Lax; Secure`; if (referralCode) - document.cookie = `referral_code=${encodeURIComponent(referralCode)}; expires=${expires}; path=/; SameSite=Lax; Secure`; + document.cookie = `referral_code=${referralCode}; expires=${expires}; path=/; SameSite=Lax; Secure`; } export function UTM() { From eb27d593582aa19d47e84241c5d8561d90bac707 Mon Sep 17 00:00:00 2001 From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com> Date: Tue, 6 Jan 2026 23:01:19 +0200 Subject: [PATCH 5/5] security: encode UTM cookies on write, decode on read --- apps/web/app/(landing)/welcome/utms.tsx | 21 +++++++++++++++------ apps/web/app/utm.tsx | 12 ++++++------ apps/web/utils/auth.ts | 7 ++++++- 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/apps/web/app/(landing)/welcome/utms.tsx b/apps/web/app/(landing)/welcome/utms.tsx index 15bdae425..02e51aaef 100644 --- a/apps/web/app/(landing)/welcome/utms.tsx +++ b/apps/web/app/(landing)/welcome/utms.tsx @@ -39,15 +39,24 @@ export function registerUtmTracking({ // See: https://nextjs.org/docs/app/api-reference/functions/after export function extractUtmValues(cookies: ReadonlyRequestCookies): UtmValues { return { - utmCampaign: cookies.get("utm_campaign")?.value, - utmMedium: cookies.get("utm_medium")?.value, - utmSource: cookies.get("utm_source")?.value, - utmTerm: cookies.get("utm_term")?.value, - affiliate: cookies.get("affiliate")?.value, - referralCode: cookies.get("referral_code")?.value, + utmCampaign: decodeCookieValue(cookies.get("utm_campaign")?.value), + utmMedium: decodeCookieValue(cookies.get("utm_medium")?.value), + utmSource: decodeCookieValue(cookies.get("utm_source")?.value), + utmTerm: decodeCookieValue(cookies.get("utm_term")?.value), + affiliate: decodeCookieValue(cookies.get("affiliate")?.value), + referralCode: decodeCookieValue(cookies.get("referral_code")?.value), }; } +function decodeCookieValue(value: string | undefined): string | undefined { + if (!value) return undefined; + try { + return decodeURIComponent(value); + } catch { + return value; + } +} + export async function fetchUserAndStoreUtms( userId: string, utmValues: UtmValues, diff --git a/apps/web/app/utm.tsx b/apps/web/app/utm.tsx index 30f842692..0e2adda8c 100644 --- a/apps/web/app/utm.tsx +++ b/apps/web/app/utm.tsx @@ -15,17 +15,17 @@ function setUtmCookies() { const expires = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toUTCString(); if (utmSource) - document.cookie = `utm_source=${utmSource}; expires=${expires}; path=/; SameSite=Lax; Secure`; + document.cookie = `utm_source=${encodeURIComponent(utmSource)}; expires=${expires}; path=/; SameSite=Lax; Secure`; if (utmMedium) - document.cookie = `utm_medium=${utmMedium}; expires=${expires}; path=/; SameSite=Lax; Secure`; + document.cookie = `utm_medium=${encodeURIComponent(utmMedium)}; expires=${expires}; path=/; SameSite=Lax; Secure`; if (utmCampaign) - document.cookie = `utm_campaign=${utmCampaign}; expires=${expires}; path=/; SameSite=Lax; Secure`; + document.cookie = `utm_campaign=${encodeURIComponent(utmCampaign)}; expires=${expires}; path=/; SameSite=Lax; Secure`; if (utmTerm) - document.cookie = `utm_term=${utmTerm}; expires=${expires}; path=/; SameSite=Lax; Secure`; + document.cookie = `utm_term=${encodeURIComponent(utmTerm)}; expires=${expires}; path=/; SameSite=Lax; Secure`; if (affiliate) - document.cookie = `affiliate=${affiliate}; expires=${expires}; path=/; SameSite=Lax; Secure`; + document.cookie = `affiliate=${encodeURIComponent(affiliate)}; expires=${expires}; path=/; SameSite=Lax; Secure`; if (referralCode) - document.cookie = `referral_code=${referralCode}; expires=${expires}; path=/; SameSite=Lax; Secure`; + document.cookie = `referral_code=${encodeURIComponent(referralCode)}; expires=${expires}; path=/; SameSite=Lax; Secure`; } export function UTM() { diff --git a/apps/web/utils/auth.ts b/apps/web/utils/auth.ts index b5f680d1b..0254c3d80 100644 --- a/apps/web/utils/auth.ts +++ b/apps/web/utils/auth.ts @@ -277,7 +277,12 @@ export async function handleReferralOnSignUp({ return; } - const referralCode = referralCookie.value; + let referralCode = referralCookie.value; + try { + referralCode = decodeURIComponent(referralCode); + } catch { + // Use original value if decoding fails + } logger.info("Processing referral for new user", { email, referralCode,