Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/app/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { validateName } from "@/lib/formValidation";
import { createClient } from "@/utils/supabase/server";
import { normalizeReferrer } from "@/utils/referrer";
import { getBaseUrl } from "@/utils/url";
import {
encodedRedirect,
Expand All @@ -28,7 +29,9 @@ export const signUpAction = async (formData: FormData, request?: Request) => {
const origin = headersList.get("origin");

// Get attribution data
const referrer = formData.get("initial_referrer")?.toString();
const referrer = normalizeReferrer(
formData.get("initial_referrer")?.toString()
);
const utmSource = formData.get("utm_source")?.toString();
const utmMedium = formData.get("utm_medium")?.toString();
const utmCampaign = formData.get("utm_campaign")?.toString();
Expand Down
18 changes: 7 additions & 11 deletions src/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,13 @@ export async function proxy(request: NextRequest) {
externalReferrer &&
request.method === "GET"
) {
response.cookies.set(
INITIAL_REFERRER_COOKIE,
encodeURIComponent(externalReferrer),
{
httpOnly: false,
maxAge: INITIAL_REFERRER_MAX_AGE,
path: "/",
sameSite: "lax",
secure: request.nextUrl.protocol === "https:",
}
);
response.cookies.set(INITIAL_REFERRER_COOKIE, externalReferrer, {
httpOnly: false,
maxAge: INITIAL_REFERRER_MAX_AGE,
path: "/",
sameSite: "lax",
secure: request.nextUrl.protocol === "https:",
});
Comment on lines +39 to +45

Copilot AI Apr 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cookie values can’t safely contain certain separator characters (notably ;, ,, and whitespace). Since this now stores the raw externalReferrer string, a referrer path containing one of those characters could lead to a truncated/invalid cookie in browsers. Consider ensuring the value is cookie-safe here (e.g. rely on a cookie serializer that encodes, or explicitly encode/sanitize before calling cookies.set).

Copilot uses AI. Check for mistakes.
}

return response;
Expand Down
20 changes: 13 additions & 7 deletions src/utils/attributionUtils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"use client";

import { normalizeReferrer } from "@/utils/referrer";

Comment thread
dnywh marked this conversation as resolved.
const UTM_STORAGE_KEY = "attribution_params";
const INITIAL_REFERRER_KEY = "initial_referrer";
const INITIAL_REFERRER_COOKIE = "initial_referrer";
Expand Down Expand Up @@ -46,11 +48,7 @@ const getCookie = (name: string): string | null => {

if (!value) return null;

try {
return decodeURIComponent(value);
} catch {
return value;
}
return normalizeReferrer(value) ?? null;
};

const getExternalDocumentReferrer = (): string | null => {
Expand All @@ -72,7 +70,7 @@ const getExternalDocumentReferrer = (): string | null => {
};

const storeInitialReferrer = (referrer: string) => {
localStorage.setItem(INITIAL_REFERRER_KEY, referrer);
localStorage.setItem(INITIAL_REFERRER_KEY, normalizeReferrer(referrer));
};

export function captureAttributionParams() {
Expand Down Expand Up @@ -125,10 +123,18 @@ export function getStoredAttributionParams(): StoredAttributionParams {
const stored = localStorage.getItem(UTM_STORAGE_KEY);
const storedInitialReferrer = localStorage.getItem(INITIAL_REFERRER_KEY);
const cookieReferrer = getCookie(INITIAL_REFERRER_COOKIE);
const initialReferrer = storedInitialReferrer ?? cookieReferrer;
const initialReferrer = storedInitialReferrer
? normalizeReferrer(storedInitialReferrer)
: cookieReferrer;

if (!storedInitialReferrer && initialReferrer) {
storeInitialReferrer(initialReferrer);
} else if (
storedInitialReferrer &&
initialReferrer &&
initialReferrer !== storedInitialReferrer
) {
storeInitialReferrer(initialReferrer);
}

return {
Expand Down
22 changes: 22 additions & 0 deletions src/utils/referrer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export function normalizeReferrer(referrer: string): string;
export function normalizeReferrer(referrer: undefined): undefined;
export function normalizeReferrer(
referrer: string | undefined
): string | undefined;
export function normalizeReferrer(referrer: string | undefined) {
if (!referrer) return undefined;

Copilot AI Apr 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

normalizeReferrer treats an empty string as “no referrer” (if (!referrer) return undefined), but the overload normalizeReferrer(referrer: string): string promises a string return for all strings. Either change the check to only treat undefined as empty (e.g. referrer === undefined) or update the overloads/return type so callers can’t assume a string when passing an empty string.

Copilot uses AI. Check for mistakes.
let current = referrer;

for (let i = 0; i < 3; i += 1) {
try {
const decoded = decodeURIComponent(current);
if (decoded === current) break;
current = decoded;
} catch {
break;
}

Copilot AI Apr 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The repeated decodeURIComponent loop can over-decode once the value is already a “raw URL”. For example, a previously double-encoded cookie can decode to a URL containing legitimate %2F/%3A sequences; the 3rd decode will turn those into //: and change the URL semantics. Consider only decoding while the string still looks like a fully-encoded URL wrapper (e.g. starts with http%3A/https%3A or contains %3A%2F%2F), and stop once you’ve reached an http(s):// form.

Copilot uses AI. Check for mistakes.
}

return current;
}
Loading