Skip to content

Commit 33087ee

Browse files
committed
feat(user): validate and sanitize UTM parameters during user creation
1 parent 9ab01e5 commit 33087ee

File tree

4 files changed

+73
-2
lines changed

4 files changed

+73
-2
lines changed

src/models/usersFactory.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ export default class UsersFactory extends AbstractModelFactory<UserDBScheme, Use
7272
const generatedPassword = password || (await UserModel.generatePassword());
7373
const hashedPassword = await UserModel.hashPassword(generatedPassword);
7474

75-
const userData: any = {
75+
const userData: Partial<UserDBScheme> = {
7676
email,
7777
password: hashedPassword,
7878
notifications: UserModel.generateDefaultNotificationsSettings(email),

src/resolvers/user.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { dateFromObjectId } from '../utils/dates';
1111
import { UserDBScheme } from '@hawk.so/types';
1212
import * as telegram from '../utils/telegram';
1313
import { MongoError } from 'mongodb';
14+
import { validateUtmParams, sanitizeUtmParams } from '../utils/analytics';
1415

1516
/**
1617
* See all types and fields here {@see ../typeDefs/user.graphql}
@@ -45,10 +46,17 @@ export default {
4546
{ email, utm }: { email: string; utm?: UserDBScheme['utm'] },
4647
{ factories }: ResolverContextBase
4748
): Promise<boolean | string> {
49+
// Validate and sanitize UTM parameters
50+
if (!validateUtmParams(utm)) {
51+
throw new UserInputError('Invalid UTM parameters provided');
52+
}
53+
54+
const sanitizedUtm = sanitizeUtmParams(utm);
55+
4856
let user;
4957

5058
try {
51-
user = await factories.usersFactory.create(email, undefined, utm);
59+
user = await factories.usersFactory.create(email, undefined, sanitizedUtm);
5260

5361
const password = user.generatedPassword!;
5462

src/utils/analytics/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export { Analytics } from './amplitude';
22
export { AnalyticsEventTypes } from './events';
3+
export { validateUtmParams, sanitizeUtmParams } from './utm';

src/utils/analytics/utm.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { UserDBScheme } from '@hawk.so/types';
2+
3+
/**
4+
* Validates UTM parameters
5+
* @param utm - Data form where user went to sign up. Used for analytics purposes
6+
* @returns boolean - true if valid, false if invalid
7+
*/
8+
export function validateUtmParams(utm: UserDBScheme['utm']): boolean {
9+
if (!utm) return true;
10+
11+
const utmKeys = ['source', 'medium', 'campaign', 'content', 'term'];
12+
const providedKeys = Object.keys(utm);
13+
14+
// Check if all provided keys are valid UTM keys
15+
const hasInvalidKeys = providedKeys.some((key) => !utmKeys.includes(key));
16+
if (hasInvalidKeys) {
17+
return false;
18+
}
19+
20+
// Check if values are strings and not too long
21+
for (const [key, value] of Object.entries(utm)) {
22+
if (value !== undefined && value !== null) {
23+
if (typeof value !== 'string' || value.length > 200) {
24+
return false;
25+
}
26+
// Basic sanitization - only allow alphanumeric, spaces, hyphens, underscores, dots
27+
if (!/^[a-zA-Z0-9\s\-_\.]+$/.test(value)) {
28+
return false;
29+
}
30+
}
31+
}
32+
33+
return true;
34+
}
35+
36+
/**
37+
* Sanitizes UTM parameters by removing invalid characters
38+
* @param utm - Data form where user went to sign up. Used for analytics purposes
39+
* @returns sanitized UTM parameters or undefined if invalid
40+
*/
41+
export function sanitizeUtmParams(utm: UserDBScheme['utm']): UserDBScheme['utm'] {
42+
if (!utm) return undefined;
43+
44+
const utmKeys = ['source', 'medium', 'campaign', 'content', 'term'];
45+
const sanitized: UserDBScheme['utm'] = {};
46+
47+
for (const [key, value] of Object.entries(utm)) {
48+
if (utmKeys.includes(key) && value && typeof value === 'string') {
49+
// Sanitize value: keep only allowed characters and limit length
50+
const cleanValue = value
51+
.replace(/[^a-zA-Z0-9\s\-_\.]/g, '')
52+
.trim()
53+
.substring(0, 200);
54+
55+
if (cleanValue.length > 0) {
56+
(sanitized as any)[key] = cleanValue;
57+
}
58+
}
59+
}
60+
61+
return Object.keys(sanitized).length > 0 ? sanitized : undefined;
62+
}

0 commit comments

Comments
 (0)