From 86fff5c728bb48370c06e2d6259914a4e208a165 Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Thu, 16 Feb 2023 11:18:05 +0200 Subject: [PATCH] Sanitize sqlite like queries and allow non-latin characters (#7141) --- app/helpers/database/index.test.ts | 43 ++++++++++++++++++++++++++++++ app/helpers/database/index.ts | 6 +++++ app/queries/servers/channel.ts | 11 ++++---- app/queries/servers/group.ts | 7 ++--- app/queries/servers/user.ts | 3 ++- 5 files changed, 61 insertions(+), 9 deletions(-) create mode 100644 app/helpers/database/index.test.ts diff --git a/app/helpers/database/index.test.ts b/app/helpers/database/index.test.ts new file mode 100644 index 0000000000..63fdc1a8b8 --- /dev/null +++ b/app/helpers/database/index.test.ts @@ -0,0 +1,43 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {sanitizeLikeString} from '.'; + +describe('Test SQLite Sanitize like string with latin and non-latin characters', () => { + const disallowed = ',./;[]!@#$%^&*()_-=+~'; + + test('test (latin)', () => { + expect(sanitizeLikeString('test123')).toBe('test123'); + expect(sanitizeLikeString(`test123${disallowed}`)).toBe(`test123${'_'.repeat(disallowed.length)}`); + }); + + test('test (arabic)', () => { + expect(sanitizeLikeString('اختبار123')).toBe('اختبار123'); + expect(sanitizeLikeString(`اختبار123${disallowed}`)).toBe(`اختبار123${'_'.repeat(disallowed.length)}`); + }); + + test('test (greek)', () => { + expect(sanitizeLikeString('δοκιμή123')).toBe('δοκιμή123'); + expect(sanitizeLikeString(`δοκιμή123${disallowed}`)).toBe(`δοκιμή123${'_'.repeat(disallowed.length)}`); + }); + + test('test (hebrew)', () => { + expect(sanitizeLikeString('חשבון123')).toBe('חשבון123'); + expect(sanitizeLikeString(`חשבון123${disallowed}`)).toBe(`חשבון123${'_'.repeat(disallowed.length)}`); + }); + + test('test (russian)', () => { + expect(sanitizeLikeString('тест123')).toBe('тест123'); + expect(sanitizeLikeString(`тест123${disallowed}`)).toBe(`тест123${'_'.repeat(disallowed.length)}`); + }); + + test('test (chinese trad)', () => { + expect(sanitizeLikeString('測試123')).toBe('測試123'); + expect(sanitizeLikeString(`測試123${disallowed}`)).toBe(`測試123${'_'.repeat(disallowed.length)}`); + }); + + test('test (japanese)', () => { + expect(sanitizeLikeString('テスト123')).toBe('テスト123'); + expect(sanitizeLikeString(`テスト123${disallowed}`)).toBe(`テスト123${'_'.repeat(disallowed.length)}`); + }); +}); diff --git a/app/helpers/database/index.ts b/app/helpers/database/index.ts index 59aefa0280..f06699d36d 100644 --- a/app/helpers/database/index.ts +++ b/app/helpers/database/index.ts @@ -1,6 +1,8 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import xRegExp from 'xregexp'; + import {General} from '@constants'; import type Model from '@nozbe/watermelondb/Model'; @@ -91,3 +93,7 @@ export const filterAndSortMyChannels = ([myChannels, channels, notifyProps]: Fil return [...mentions, ...unreads, ...mutedMentions]; }; + +// Matches letters from any alphabet and numbers +const sqliteLikeStringRegex = xRegExp('[^\\p{L}\\p{Nd}]', 'g'); +export const sanitizeLikeString = (value: string) => value.replace(sqliteLikeStringRegex, '_'); diff --git a/app/queries/servers/channel.ts b/app/queries/servers/channel.ts index ad6c5a3303..1acabd0f4c 100644 --- a/app/queries/servers/channel.ts +++ b/app/queries/servers/channel.ts @@ -9,6 +9,7 @@ import {map as map$, switchMap, distinctUntilChanged} from 'rxjs/operators'; import {General, Permissions} from '@constants'; import {MM_TABLES} from '@constants/database'; +import {sanitizeLikeString} from '@helpers/database'; import {hasPermission} from '@utils/role'; import {prepareDeletePost} from './post'; @@ -523,7 +524,7 @@ export function queryMyRecentChannels(database: Database, take: number) { export const observeDirectChannelsByTerm = (database: Database, term: string, take = 20, matchStart = false) => { const onlyDMs = term.startsWith('@') ? "AND c.type='D'" : ''; - const value = Q.sanitizeLikeString(term.startsWith('@') ? term.substring(1) : term); + const value = sanitizeLikeString(term.startsWith('@') ? term.substring(1) : term); let username = `u.username LIKE '${value}%'`; let displayname = `c.display_name LIKE '${value}%'`; if (!matchStart) { @@ -549,7 +550,7 @@ export const observeDirectChannelsByTerm = (database: Database, term: string, ta export const observeNotDirectChannelsByTerm = (database: Database, term: string, take = 20, matchStart = false) => { const teammateNameSetting = observeTeammateNameDisplay(database); - const value = Q.sanitizeLikeString(term.startsWith('@') ? term.substring(1) : term); + const value = sanitizeLikeString(term.startsWith('@') ? term.substring(1) : term); let username = `u.username LIKE '${value}%'`; let nickname = `u.nickname LIKE '${value}%'`; let displayname = `(u.first_name || ' ' || u.last_name) LIKE '${value}%'`; @@ -590,7 +591,7 @@ export const observeJoinedChannelsByTerm = (database: Database, term: string, ta return of$([]); } - const value = Q.sanitizeLikeString(term); + const value = sanitizeLikeString(term); let displayname = `c.display_name LIKE '${value}%'`; if (!matchStart) { displayname = `c.display_name LIKE '%${value}%' AND c.display_name NOT LIKE '${value}%'`; @@ -608,7 +609,7 @@ export const observeArchiveChannelsByTerm = (database: Database, term: string, t return of$([]); } - const value = Q.sanitizeLikeString(term); + const value = sanitizeLikeString(term); const displayname = `%${value}%`; return database.get(MY_CHANNEL).query( Q.on(CHANNEL, Q.and( @@ -639,7 +640,7 @@ export const observeChannelsByLastPostAt = (database: Database, myChannels: MyCh }; export const queryChannelsForAutocomplete = (database: Database, matchTerm: string, isSearch: boolean, teamId: string) => { - const likeTerm = `%${Q.sanitizeLikeString(matchTerm)}%`; + const likeTerm = `%${sanitizeLikeString(matchTerm)}%`; const clauses: Q.Clause[] = []; if (isSearch) { clauses.push( diff --git a/app/queries/servers/group.ts b/app/queries/servers/group.ts index 151e61da69..5b659246a8 100644 --- a/app/queries/servers/group.ts +++ b/app/queries/servers/group.ts @@ -4,6 +4,7 @@ import {Database, Q} from '@nozbe/watermelondb'; import {MM_TABLES} from '@constants/database'; +import {sanitizeLikeString} from '@helpers/database'; import type GroupModel from '@typings/database/models/servers/group'; import type GroupChannelModel from '@typings/database/models/servers/group_channel'; @@ -14,7 +15,7 @@ const {SERVER: {GROUP, GROUP_CHANNEL, GROUP_MEMBERSHIP, GROUP_TEAM}} = MM_TABLES export const queryGroupsByName = (database: Database, name: string) => { return database.collections.get(GROUP).query( - Q.where('name', Q.like(`%${Q.sanitizeLikeString(name)}%`)), + Q.where('name', Q.like(`%${sanitizeLikeString(name)}%`)), ); }; @@ -27,14 +28,14 @@ export const queryGroupsByNames = (database: Database, names: string[]) => { export const queryGroupsByNameInTeam = (database: Database, name: string, teamId: string) => { return database.collections.get(GROUP).query( Q.on(GROUP_TEAM, 'team_id', teamId), - Q.where('name', Q.like(`%${Q.sanitizeLikeString(name)}%`)), + Q.where('name', Q.like(`%${sanitizeLikeString(name)}%`)), ); }; export const queryGroupsByNameInChannel = (database: Database, name: string, channelId: string) => { return database.collections.get(GROUP).query( Q.on(GROUP_CHANNEL, 'channel_id', channelId), - Q.where('name', Q.like(`%${Q.sanitizeLikeString(name)}%`)), + Q.where('name', Q.like(`%${sanitizeLikeString(name)}%`)), ); }; diff --git a/app/queries/servers/user.ts b/app/queries/servers/user.ts index e9818f35bf..2a76096766 100644 --- a/app/queries/servers/user.ts +++ b/app/queries/servers/user.ts @@ -7,6 +7,7 @@ import {distinctUntilChanged, switchMap} from 'rxjs/operators'; import {MM_TABLES} from '@constants/database'; import {getTeammateNameDisplaySetting} from '@helpers/api/preference'; +import {sanitizeLikeString} from '@helpers/database'; import {queryDisplayNamePreferences} from './preference'; import {observeCurrentUserId, observeLicense, getCurrentUserId, getConfig, getLicense, observeConfigValue} from './system'; @@ -86,7 +87,7 @@ export async function getTeammateNameDisplay(database: Database) { export const queryUsersLike = (database: Database, likeUsername: string) => { return database.get(USER).query( Q.where('username', Q.like( - `%${Q.sanitizeLikeString(likeUsername)}%`, + `%${sanitizeLikeString(likeUsername)}%`, )), ); };