Reject reactions with invalid number of graphemes

This commit is contained in:
Evan Hahn 2021-04-07 12:04:42 -05:00 committed by Josh Perez
parent 95482fbf31
commit f615b1a75f
5 changed files with 92 additions and 2 deletions

View file

@ -11,6 +11,7 @@ import * as refreshSenderCertificate from './refreshSenderCertificate';
import { SenderCertificateMode } from './metadata/SecretSessionCipher';
import { routineProfileRefresh } from './routineProfileRefresh';
import { isMoreRecentThan, isOlderThan } from './util/timestamp';
import { isValidReactionEmoji } from './reactions/isValidReactionEmoji';
const MAX_ATTACHMENT_DOWNLOAD_AGE = 3600 * 72 * 1000;
@ -2601,6 +2602,13 @@ export async function startApp(): Promise<void> {
);
const { reaction } = data.message;
if (!isValidReactionEmoji(reaction.emoji)) {
window.log.warn('Received an invalid reaction emoji. Dropping it');
confirm();
return Promise.resolve();
}
window.log.info(
'Queuing incoming reaction for',
reaction.targetTimestamp
@ -2894,6 +2902,13 @@ export async function startApp(): Promise<void> {
);
const { reaction } = data.message;
if (!isValidReactionEmoji(reaction.emoji)) {
window.log.warn('Received an invalid reaction emoji. Dropping it');
event.confirm();
return Promise.resolve();
}
window.log.info('Queuing sent reaction for', reaction.targetTimestamp);
const reactionModel = window.Whisper.Reactions.add({
emoji: reaction.emoji,

View file

@ -0,0 +1,22 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import emojiRegex from 'emoji-regex/es2015/RGI_Emoji';
import { getGraphemes } from '../util/grapheme';
import { take, size } from '../util/iterables';
export function isValidReactionEmoji(value: unknown): value is string {
if (typeof value !== 'string') {
return false;
}
// This is effectively `countGraphemes(value) === 1`, but doesn't require iterating
// through an extremely long string.
const graphemes = getGraphemes(value);
const truncatedGraphemes = take(graphemes, 2);
if (size(truncatedGraphemes) !== 1) {
return false;
}
return emojiRegex().test(value);
}

View file

@ -0,0 +1,31 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import { isValidReactionEmoji } from '../../reactions/isValidReactionEmoji';
describe('isValidReactionEmoji', () => {
it('returns false for non-strings', () => {
assert.isFalse(isValidReactionEmoji(undefined));
assert.isFalse(isValidReactionEmoji(null));
assert.isFalse(isValidReactionEmoji(1));
});
it("returns false for strings that aren't a single emoji", () => {
assert.isFalse(isValidReactionEmoji(''));
assert.isFalse(isValidReactionEmoji('a'));
assert.isFalse(isValidReactionEmoji('abc'));
assert.isFalse(isValidReactionEmoji('💩💩'));
assert.isFalse(isValidReactionEmoji('🇸'));
assert.isFalse(isValidReactionEmoji(''));
});
it('returns true for strings that are exactly 1 emoji', () => {
assert.isTrue(isValidReactionEmoji('🇺🇸'));
assert.isTrue(isValidReactionEmoji('👩‍❤️‍👩'));
});
});

View file

@ -3,9 +3,26 @@
import { assert } from 'chai';
import { count } from '../../util/grapheme';
import { getGraphemes, count } from '../../util/grapheme';
describe('grapheme utilities', () => {
describe('getGraphemes', () => {
it('returns extended graphemes in a string', () => {
assert.deepEqual([...getGraphemes('')], []);
assert.deepEqual([...getGraphemes('hello')], [...'hello']);
assert.deepEqual(
[...getGraphemes('Bokmål')],
['B', 'o', 'k', 'm', 'å', 'l']
);
assert.deepEqual([...getGraphemes('💩💩💩')], ['💩', '💩', '💩']);
assert.deepEqual([...getGraphemes('👩‍❤️‍👩')], ['👩‍❤️‍👩']);
assert.deepEqual([...getGraphemes('👌🏽👌🏾👌🏿')], ['👌🏽', '👌🏾', '👌🏿']);
assert.deepEqual([...getGraphemes('L̷̳͔̲͝Ģ̵̮̯̤̩̙͍̬̟͉̹̘̹͍͈̮̦̰̣͟͝O̶̴̮̻̮̗͘͡!̴̷̟͓͓')], ['L̷̳͔̲͝', 'Ģ̵̮̯̤̩̙͍̬̟͉̹̘̹͍͈̮̦̰̣͟͝', 'O̶̴̮̻̮̗͘͡', '!̴̷̟͓͓']);
});
});
describe('count', () => {
it('returns the number of extended graphemes in a string (not necessarily the length)', () => {
// These tests modified [from iOS][0].

View file

@ -1,7 +1,12 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { size } from './iterables';
import { map, size } from './iterables';
export function getGraphemes(str: string): Iterable<string> {
const segments = new Intl.Segmenter().segment(str);
return map(segments, s => s.segment);
}
export function count(str: string): number {
const segments = new Intl.Segmenter().segment(str);