From b943a23def003747ccbd65282515925fe175e2f6 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Fri, 1 Apr 2022 02:21:12 -0700 Subject: [PATCH] packages/grafana-data/text: Improve escaping for special characters (#47066) (#47148) FilterInput escapes all input strings for special characters that might be used in a RegExp by calling escapeStringForRegex, and using unEscapeStringFromRegex for display. Both of these functions used string.prototype.replace() for escaping. replace() only replaces the first occurence of a search string unless called with a global RegExp. The output of these functions was not necessarily safe to compile into a RegExp literal. This change creates RegExps for escapeStringForRegex and unEscapeStringFromRegex to match all occurrences of the special characters instead of just their first occurrence. This makes a variety of strings safe for RegExp compilation. (cherry picked from commit c5cfc1645a3dac4a639b43577427fe9a9a42a203) Co-authored-by: Joe Blubaugh --- packages/grafana-data/src/text/string.test.ts | 30 +++++++++++++++---- packages/grafana-data/src/text/string.ts | 10 +++++-- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/packages/grafana-data/src/text/string.test.ts b/packages/grafana-data/src/text/string.test.ts index b9cd3eb0866..c0ab2b3a854 100644 --- a/packages/grafana-data/src/text/string.test.ts +++ b/packages/grafana-data/src/text/string.test.ts @@ -57,12 +57,32 @@ describe('stringToMs', () => { }); }); +describe('[un]escapeStringForRegex', () => { + it.each([ + '[]', + '\\', + '[(abc])', + 'onetwothree', + '', + 'string\\with\\backslash', + 'everyspecialchar([{])}.,/?&*-^&<>#', + ])('should be symmetric', (input) => { + const output = unEscapeStringFromRegex(escapeStringForRegex(input)); + expect(output).toEqual(input); + }); +}); + describe('escapeStringForRegex', () => { - describe('when using a string with special chars', () => { - it('then all special chars should be escaped', () => { - const result = escapeStringForRegex('([{}])|*+-.?<>#&^$'); - expect(result).toBe('\\(\\[\\{\\}\\]\\)\\|\\*\\+\\-\\.\\?\\<\\>\\#\\&\\^\\$'); - }); + it.each([ + '[[[', + '[]\\', + '[(abc])', + 'onetwothree', + '', + 'string\\with\\backslash', + 'everyspecialchar([{])}.,/?&*-^&<>#', + ])('should always produce output that compiles', (value) => { + expect(() => new RegExp(escapeStringForRegex(value))).not.toThrowError(); }); describe('when using a string without special chars', () => { diff --git a/packages/grafana-data/src/text/string.ts b/packages/grafana-data/src/text/string.ts index 79cf3ae05cd..12f628c7cb0 100644 --- a/packages/grafana-data/src/text/string.ts +++ b/packages/grafana-data/src/text/string.ts @@ -1,12 +1,16 @@ import { camelCase } from 'lodash'; -const specialChars = ['(', '[', '{', '}', ']', ')', '|', '*', '+', '-', '.', '?', '<', '>', '#', '&', '^', '$']; + +const specialChars = ['(', '[', '{', '}', ']', ')', '\\', '|', '*', '+', '-', '.', '?', '<', '>', '#', '&', '^', '$']; +const specialMatcher = '([\\' + specialChars.join('\\') + '])'; +const specialCharEscape = new RegExp(specialMatcher, 'g'); +const specialCharUnescape = new RegExp('(\\\\)' + specialMatcher, 'g'); export const escapeStringForRegex = (value: string) => { if (!value) { return value; } - return specialChars.reduce((escaped, currentChar) => escaped.replace(currentChar, '\\' + currentChar), value); + return value.replace(specialCharEscape, '\\$1'); }; export const unEscapeStringFromRegex = (value: string) => { @@ -14,7 +18,7 @@ export const unEscapeStringFromRegex = (value: string) => { return value; } - return specialChars.reduce((escaped, currentChar) => escaped.replace('\\' + currentChar, currentChar), value); + return value.replace(specialCharUnescape, '$2'); }; export function stringStartsAsRegEx(str: string): boolean {