mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 17:00:15 +01:00
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:
parent
b48c618f32
commit
1378eb5097
3 changed files with 194 additions and 8 deletions
|
|
@ -11,6 +11,20 @@ function errorsToString(errors) {
|
||||||
.join(' ');
|
.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({
|
const loginSchema = z.object({
|
||||||
email: z.string().email(),
|
email: z.string().email(),
|
||||||
password: z
|
password: z
|
||||||
|
|
@ -26,14 +40,7 @@ const registerSchema = z
|
||||||
.object({
|
.object({
|
||||||
name: z.string().min(3).max(80),
|
name: z.string().min(3).max(80),
|
||||||
username: z
|
username: z
|
||||||
.union([
|
.union([z.literal(''), usernameSchema])
|
||||||
z.literal(''),
|
|
||||||
z
|
|
||||||
.string()
|
|
||||||
.min(2)
|
|
||||||
.max(80)
|
|
||||||
.regex(/^[a-zA-Z0-9_.-@#$%&*() ]+$/),
|
|
||||||
])
|
|
||||||
.transform((value) => (value === '' ? null : value))
|
.transform((value) => (value === '' ? null : value))
|
||||||
.optional()
|
.optional()
|
||||||
.nullable(),
|
.nullable(),
|
||||||
|
|
|
||||||
|
|
@ -260,6 +260,184 @@ describe('Zod Schemas', () => {
|
||||||
});
|
});
|
||||||
expect(result.success).toBe(true);
|
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', () => {
|
describe('errorsToString', () => {
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ const MinimalIcon: React.FC<IconProps> = (props) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
data-testid="convo-icon"
|
||||||
title={name}
|
title={name}
|
||||||
style={{
|
style={{
|
||||||
width: size,
|
width: size,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue