fix: Allow Latin-based Special Characters in Username (#969)

* fix: username validation

* fix: add data-testid to fix e2e workflow
This commit is contained in:
Danny Avila 2023-09-18 16:57:12 -04:00 committed by GitHub
parent b48c618f32
commit 1378eb5097
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 194 additions and 8 deletions

View file

@ -11,6 +11,20 @@ function errorsToString(errors) {
.join(' ');
}
const allowedCharactersRegex = /^[a-zA-Z0-9_.@#$%&*()\p{Script=Latin}\p{Script=Common}]+$/u;
const injectionPatternsRegex = /('|--|\$ne|\$gt|\$lt|\$or|\{|\}|\*|;|<|>|\/|=)/i;
const usernameSchema = z
.string()
.min(2)
.max(80)
.refine((value) => allowedCharactersRegex.test(value), {
message: 'Invalid characters in username',
})
.refine((value) => !injectionPatternsRegex.test(value), {
message: 'Potential injection attack detected',
});
const loginSchema = z.object({
email: z.string().email(),
password: z
@ -26,14 +40,7 @@ const registerSchema = z
.object({
name: z.string().min(3).max(80),
username: z
.union([
z.literal(''),
z
.string()
.min(2)
.max(80)
.regex(/^[a-zA-Z0-9_.-@#$%&*() ]+$/),
])
.union([z.literal(''), usernameSchema])
.transform((value) => (value === '' ? null : value))
.optional()
.nullable(),

View file

@ -260,6 +260,184 @@ describe('Zod Schemas', () => {
});
expect(result.success).toBe(true);
});
it('should handle username with special characters from various languages', () => {
const usernames = [
// General
'éèäöü',
// German
'Jöhn.Döe@',
'Jöhn_Ü',
'Jöhnß',
// French
'Jéan-Piérre',
'Élève',
'Fiançée',
'Mère',
// Spanish
'Niño',
'Señor',
'Muñoz',
// Portuguese
'João',
'Coração',
'Pão',
// Italian
'Pietro',
'Bambino',
'Forlì',
// Romanian
'Mâncare',
'Școală',
'Țară',
// Catalan
'Niç',
'Màquina',
'Çap',
// Swedish
'Fjärran',
'Skål',
'Öland',
// Norwegian
'Blåbær',
'Fjord',
'Årstid',
// Danish
'Flød',
'Søster',
'Århus',
// Icelandic
'Þór',
'Ætt',
'Öx',
// Turkish
'Şehir',
'Çocuk',
'Gözlük',
// Polish
'Łódź',
'Część',
'Świat',
// Czech
'Čaj',
'Řeka',
'Život',
// Slovak
'Kočka',
'Ľudia',
'Žaba',
// Croatian
'Čovjek',
'Šuma',
'Žaba',
// Hungarian
'Tűz',
'Ősz',
'Ünnep',
// Finnish
'Mäki',
'Yö',
'Äiti',
// Estonian
'Tänav',
'Öö',
'Ülikool',
// Latvian
'Ēka',
'Ūdens',
'Čempions',
// Lithuanian
'Ūsas',
'Ąžuolas',
'Čia',
// Dutch
'Maïs',
'Geërfd',
'Coördinatie',
];
const failingUsernames = usernames.reduce((acc, username) => {
const result = registerSchema.safeParse({
name: 'John Doe',
username,
email: 'john@example.com',
password: 'password123',
confirm_password: 'password123',
});
if (!result.success) {
acc.push({ username, error: result.error });
}
return acc;
}, []);
if (failingUsernames.length > 0) {
console.log('Failing Usernames:', failingUsernames);
}
expect(failingUsernames).toEqual([]);
});
it('should reject invalid usernames', () => {
const invalidUsernames = [
'Дмитрий', // Cyrillic characters
'محمد', // Arabic characters
'张伟', // Chinese characters
'john{doe}', // Contains `{` and `}`
'j', // Only one character
'a'.repeat(81), // More than 80 characters
'\' OR \'1\'=\'1\'; --', // SQL Injection
'{$ne: null}', // MongoDB Injection
'<script>alert("XSS")</script>', // Basic XSS
'"><script>alert("XSS")</script>', // XSS breaking out of an attribute
'"><img src=x onerror=alert("XSS")>', // XSS using an image tag
];
const passingUsernames = [];
const failingUsernames = invalidUsernames.reduce((acc, username) => {
const result = registerSchema.safeParse({
name: 'John Doe',
username,
email: 'john@example.com',
password: 'password123',
confirm_password: 'password123',
});
if (!result.success) {
acc.push({ username, error: result.error });
}
if (result.success) {
passingUsernames.push({ username });
}
return acc;
}, []);
expect(failingUsernames.length).toEqual(invalidUsernames.length); // They should match since all invalidUsernames should fail.
});
});
describe('errorsToString', () => {

View file

@ -35,6 +35,7 @@ const MinimalIcon: React.FC<IconProps> = (props) => {
return (
<div
data-testid="convo-icon"
title={name}
style={{
width: size,