Skip to content

Commit e8f5da7

Browse files
fix(analytics): DATA-13050 Generate text correctly when product name has number sign in name
1 parent 2cd4478 commit e8f5da7

File tree

1 file changed

+44
-6
lines changed

1 file changed

+44
-6
lines changed

src/app/api/app/load/route.ts

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,45 @@ const jwtSchema = z.object({
2929
channel_id: z.number().nullable(),
3030
});
3131

32-
export async function GET(request: NextRequest) {
33-
function appendExchangeToken(url: string, token: string): string {
34-
const delimiter = new URL(url, env.APP_ORIGIN).search ? '&' : '?';
32+
/**
33+
* It can be provided a URL where `product_name` contains raw reserved characters
34+
* (e.g. "Widget #2", "AT&T", "plus+sign"). These can be interpreted as URL delimiters
35+
* (`#` fragment, `&` query separator, `+` becomes space in x-www-form-urlencoded parsing),
36+
* truncating or mutating the query param value.
37+
*
38+
* We sanitize the value in-place so Next can receive the full name.
39+
*/
40+
function sanitizeQueryParamValue(value: string): string {
41+
const withSafePercents = value.replace(/%(?![0-9A-Fa-f]{2})/g, '%25');
3542

36-
return `${url}${delimiter}exchangeToken=${token}`;
37-
}
43+
return withSafePercents
44+
.replaceAll('+', '%2B')
45+
.replaceAll('#', '%23')
46+
.replaceAll('&', '%26')
47+
.replaceAll(' ', '%20');
48+
}
49+
50+
function sanitizeQueryParamValueInPath(path: string, paramName: string): string {
51+
const key = `${paramName}=`;
52+
const keyIdx = path.indexOf(key);
53+
if (keyIdx === -1) return path;
54+
55+
const valueStart = keyIdx + key.length;
56+
const remainder = path.slice(valueStart);
57+
const delimiterMatch = remainder.match(/&[A-Za-z0-9_.~-]+=/);
58+
const endIdx =
59+
delimiterMatch && typeof delimiterMatch.index === 'number'
60+
? valueStart + delimiterMatch.index
61+
: path.length;
3862

63+
const before = path.slice(0, valueStart);
64+
const value = path.slice(valueStart, endIdx);
65+
const after = path.slice(endIdx);
66+
67+
return `${before}${sanitizeQueryParamValue(value)}${after}`;
68+
}
69+
70+
export async function GET(request: NextRequest) {
3971
const parsedParams = queryParamSchema.safeParse(
4072
Object.fromEntries(request.nextUrl.searchParams)
4173
);
@@ -64,8 +96,14 @@ export async function GET(request: NextRequest) {
6496
});
6597

6698
const exchangeToken = await db.saveClientToken(clientToken);
99+
const safePath = sanitizeQueryParamValueInPath(
100+
sanitizeQueryParamValueInPath(path, 'product_name'),
101+
'productName'
102+
);
103+
const redirectUrl = new URL(safePath, env.APP_ORIGIN);
104+
redirectUrl.searchParams.set('exchangeToken', exchangeToken);
67105

68-
return NextResponse.redirect(new URL(appendExchangeToken(path, exchangeToken), env.APP_ORIGIN), {
106+
return NextResponse.redirect(redirectUrl, {
69107
status: 302,
70108
statusText: 'Found',
71109
});

0 commit comments

Comments
 (0)