Skip to content

Commit 233e231

Browse files
authored
fix referrer encoding (#44)
* fix referrer encoding * fix referrer type guard * address referrer review * refine referrer normalisation * remove duplicate rules * harden referrer normalisation * address referrer review follow-up * trim referrer cookie values
1 parent 74e913e commit 233e231

11 files changed

Lines changed: 94 additions & 42 deletions

File tree

.cursor/rules/file-format-preference.mdc

Lines changed: 0 additions & 6 deletions
This file was deleted.

src/app/actions.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"use server";
22

33
import { validateName } from "@/lib/formValidation";
4+
import { getSafeHttpReferrer } from "@/utils/referrer";
45
import { createClient } from "@/utils/supabase/server";
56
import { getBaseUrl } from "@/utils/url";
67
import {
@@ -28,7 +29,9 @@ export const signUpAction = async (formData: FormData, request?: Request) => {
2829
const origin = headersList.get("origin");
2930

3031
// Get attribution data
31-
const referrer = formData.get("initial_referrer")?.toString();
32+
const referrer = getSafeHttpReferrer(
33+
formData.get("initial_referrer")?.toString()
34+
);
3235
const utmSource = formData.get("utm_source")?.toString();
3336
const utmMedium = formData.get("utm_medium")?.toString();
3437
const utmCampaign = formData.get("utm_campaign")?.toString();

src/app/auth/callback/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { createClient } from "@/utils/supabase/server";
2-
import { appendSuccessParam, normalizeNextPath } from "@/utils/authRedirects";
2+
import { appendSuccessParam, normaliseNextPath } from "@/utils/authRedirects";
33
import { NextResponse } from "next/server";
44

55
const isAuthDebugEnabled = process.env.NEXT_PUBLIC_AUTH_DEBUG === "true";
@@ -23,7 +23,7 @@ export async function GET(request: Request) {
2323
const requestedNextPath =
2424
requestUrl.searchParams.get("next") ??
2525
requestUrl.searchParams.get("redirect_to");
26-
const nextPath = normalizeNextPath(requestedNextPath, "/profile");
26+
const nextPath = normaliseNextPath(requestedNextPath, "/profile");
2727

2828
if (code) {
2929
const supabase = await createClient();

src/app/auth/confirm/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {
33
appendSuccessParam,
44
getDefaultNextPathByType,
55
isSupportedEmailAuthType,
6-
normalizeNextPath,
6+
normaliseNextPath,
77
} from "@/utils/authRedirects";
88
import { NextResponse } from "next/server";
99

@@ -35,7 +35,7 @@ export async function GET(request: Request) {
3535
requestUrl.searchParams.get("next") ??
3636
requestUrl.searchParams.get("redirect_to");
3737
const defaultNextPath = getDefaultNextPathByType(authType);
38-
const nextPath = normalizeNextPath(requestedNextPath, defaultNextPath);
38+
const nextPath = normaliseNextPath(requestedNextPath, defaultNextPath);
3939

4040
if (!tokenHash || !isSupportedEmailAuthType(authType)) {
4141
debugAuth("invalid-confirm-query", {

src/app/auth/session/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {
33
appendSuccessParam,
44
getDefaultNextPathByType,
55
isSupportedEmailAuthType,
6-
normalizeNextPath,
6+
normaliseNextPath,
77
} from "@/utils/authRedirects";
88
import { NextResponse } from "next/server";
99

@@ -54,7 +54,7 @@ export async function POST(request: Request) {
5454
}
5555

5656
const defaultNextPath = getDefaultNextPathByType(type);
57-
const nextPath = normalizeNextPath(
57+
const nextPath = normaliseNextPath(
5858
typeof body?.next === "string" ? body.next : null,
5959
defaultNextPath
6060
);

src/components/AuthHashCompletion.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useEffect } from "react";
44
import {
55
getDefaultNextPathByType,
66
isSupportedEmailAuthType,
7-
normalizeNextPath,
7+
normaliseNextPath,
88
} from "@/utils/authRedirects";
99

1010
const INVALID_LINK_MESSAGE =
@@ -37,7 +37,7 @@ export default function AuthHashCompletion() {
3737
queryParams.get("next") ?? queryParams.get("redirect_to");
3838
const requestedType = queryParams.get("type") ?? hashType;
3939
const defaultNextPath = getDefaultNextPathByType(requestedType);
40-
const nextPath = normalizeNextPath(preferredNextPath, defaultNextPath);
40+
const nextPath = normaliseNextPath(preferredNextPath, defaultNextPath);
4141

4242
const authCode = queryParams.get("code");
4343
const hasAuthHashPayload =
@@ -126,7 +126,7 @@ export default function AuthHashCompletion() {
126126
return;
127127
}
128128

129-
const typedNextPath = normalizeNextPath(
129+
const typedNextPath = normaliseNextPath(
130130
preferredNextPath,
131131
getDefaultNextPathByType(type)
132132
);
@@ -177,7 +177,7 @@ export default function AuthHashCompletion() {
177177
return;
178178
}
179179

180-
const resolvedNextPath = normalizeNextPath(data.next, typedNextPath);
180+
const resolvedNextPath = normaliseNextPath(data.next, typedNextPath);
181181
debugAuth("session-finalized", {
182182
type,
183183
nextPath: resolvedNextPath,

src/proxy.ts

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -36,17 +36,13 @@ export async function proxy(request: NextRequest) {
3636
externalReferrer &&
3737
request.method === "GET"
3838
) {
39-
response.cookies.set(
40-
INITIAL_REFERRER_COOKIE,
41-
encodeURIComponent(externalReferrer),
42-
{
43-
httpOnly: false,
44-
maxAge: INITIAL_REFERRER_MAX_AGE,
45-
path: "/",
46-
sameSite: "lax",
47-
secure: request.nextUrl.protocol === "https:",
48-
}
49-
);
39+
response.cookies.set(INITIAL_REFERRER_COOKIE, externalReferrer, {
40+
httpOnly: false,
41+
maxAge: INITIAL_REFERRER_MAX_AGE,
42+
path: "/",
43+
sameSite: "lax",
44+
secure: request.nextUrl.protocol === "https:",
45+
});
5046
}
5147

5248
return response;

src/utils/attributionUtils.ts

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"use client";
22

3+
import { normaliseReferrer } from "@/utils/referrer";
4+
35
const UTM_STORAGE_KEY = "attribution_params";
46
const INITIAL_REFERRER_KEY = "initial_referrer";
57
const INITIAL_REFERRER_COOKIE = "initial_referrer";
@@ -46,11 +48,12 @@ const getCookie = (name: string): string | null => {
4648

4749
if (!value) return null;
4850

49-
try {
50-
return decodeURIComponent(value);
51-
} catch {
52-
return value;
53-
}
51+
const cleanedValue = normaliseReferrer(value);
52+
const trimmedValue = cleanedValue.trim();
53+
54+
if (!trimmedValue) return null;
55+
56+
return trimmedValue;
5457
};
5558

5659
const getExternalDocumentReferrer = (): string | null => {
@@ -72,7 +75,7 @@ const getExternalDocumentReferrer = (): string | null => {
7275
};
7376

7477
const storeInitialReferrer = (referrer: string) => {
75-
localStorage.setItem(INITIAL_REFERRER_KEY, referrer);
78+
localStorage.setItem(INITIAL_REFERRER_KEY, normaliseReferrer(referrer));
7679
};
7780

7881
export function captureAttributionParams() {
@@ -125,9 +128,18 @@ export function getStoredAttributionParams(): StoredAttributionParams {
125128
const stored = localStorage.getItem(UTM_STORAGE_KEY);
126129
const storedInitialReferrer = localStorage.getItem(INITIAL_REFERRER_KEY);
127130
const cookieReferrer = getCookie(INITIAL_REFERRER_COOKIE);
128-
const initialReferrer = storedInitialReferrer ?? cookieReferrer;
131+
const initialReferrer =
132+
storedInitialReferrer !== null
133+
? normaliseReferrer(storedInitialReferrer)
134+
: cookieReferrer;
129135

130-
if (!storedInitialReferrer && initialReferrer) {
136+
if (storedInitialReferrer === null && initialReferrer) {
137+
storeInitialReferrer(initialReferrer);
138+
} else if (
139+
storedInitialReferrer !== null &&
140+
initialReferrer &&
141+
initialReferrer !== storedInitialReferrer
142+
) {
131143
storeInitialReferrer(initialReferrer);
132144
}
133145

src/utils/authRedirects.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export const getDefaultNextPathByType = (type: string | null | undefined) => {
2424
return "/profile";
2525
};
2626

27-
export const normalizeNextPath = (
27+
export const normaliseNextPath = (
2828
candidatePath: string | null | undefined,
2929
fallbackPath: string
3030
) => {
@@ -49,8 +49,8 @@ export const normalizeNextPath = (
4949
};
5050

5151
export const appendSuccessParam = (path: string, successValue: string) => {
52-
const normalizedPath = normalizeNextPath(path, "/profile");
53-
const url = new URL(normalizedPath, "https://www.peels.app");
52+
const normalisedPath = normaliseNextPath(path, "/profile");
53+
const url = new URL(normalisedPath, "https://www.peels.app");
5454
url.searchParams.set("success", successValue);
5555
return `${url.pathname}${url.search}${url.hash}`;
5656
};

src/utils/referrer.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
const encodedReferrerPrefix = /^https?%(?:25)*3a%(?:25)*2f%(?:25)*2f/i;
2+
const controlCharacters = /[\u0000-\u001f\u007f]/g;
3+
const MAX_REFERRER_LENGTH = 512;
4+
5+
export function normaliseReferrer(referrer: string): string;
6+
export function normaliseReferrer(referrer: undefined): undefined;
7+
export function normaliseReferrer(
8+
referrer: string | undefined
9+
): string | undefined;
10+
export function normaliseReferrer(referrer: string | undefined) {
11+
if (referrer === undefined) return undefined;
12+
13+
let current = referrer.trim();
14+
15+
for (let i = 0; i < 3 && encodedReferrerPrefix.test(current); i += 1) {
16+
try {
17+
const decoded = decodeURIComponent(current);
18+
if (decoded === current) break;
19+
current = decoded;
20+
} catch {
21+
break;
22+
}
23+
}
24+
25+
return current.replace(controlCharacters, "").slice(0, MAX_REFERRER_LENGTH);
26+
}
27+
28+
export function getSafeHttpReferrer(referrer: string | undefined) {
29+
const cleanedReferrer = normaliseReferrer(referrer)?.trim();
30+
31+
if (!cleanedReferrer) return undefined;
32+
33+
try {
34+
const referrerUrl = new URL(cleanedReferrer);
35+
36+
if (!["http:", "https:"].includes(referrerUrl.protocol)) {
37+
return undefined;
38+
}
39+
40+
referrerUrl.search = "";
41+
referrerUrl.hash = "";
42+
43+
return referrerUrl.toString().slice(0, MAX_REFERRER_LENGTH);
44+
} catch {
45+
return undefined;
46+
}
47+
}

0 commit comments

Comments
 (0)