packages/grafana-data/text: Improve escaping for special characters (#47066)

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.
This commit is contained in:
Joe Blubaugh 2022-03-31 22:34:34 +08:00 committed by GitHub
parent 5a87d12e8c
commit c5cfc1645a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 32 additions and 8 deletions

View File

@ -57,12 +57,32 @@ describe('stringToMs', () => {
}); });
}); });
describe('[un]escapeStringForRegex', () => {
it.each([
'[]',
'\\',
'[(abc])',
'onetwothree',
'<namedgroup}(this is not a regex>',
'string\\with\\backslash',
'everyspecialchar([{])}.,/?&*-^&<>#',
])('should be symmetric', (input) => {
const output = unEscapeStringFromRegex(escapeStringForRegex(input));
expect(output).toEqual(input);
});
});
describe('escapeStringForRegex', () => { describe('escapeStringForRegex', () => {
describe('when using a string with special chars', () => { it.each([
it('then all special chars should be escaped', () => { '[[[',
const result = escapeStringForRegex('([{}])|*+-.?<>#&^$'); '[]\\',
expect(result).toBe('\\(\\[\\{\\}\\]\\)\\|\\*\\+\\-\\.\\?\\<\\>\\#\\&\\^\\$'); '[(abc])',
}); 'onetwothree',
'<namedgroup}(this is not a regex>',
'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', () => { describe('when using a string without special chars', () => {

View File

@ -1,12 +1,16 @@
import { camelCase } from 'lodash'; 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) => { export const escapeStringForRegex = (value: string) => {
if (!value) { if (!value) {
return value; return value;
} }
return specialChars.reduce((escaped, currentChar) => escaped.replace(currentChar, '\\' + currentChar), value); return value.replace(specialCharEscape, '\\$1');
}; };
export const unEscapeStringFromRegex = (value: string) => { export const unEscapeStringFromRegex = (value: string) => {
@ -14,7 +18,7 @@ export const unEscapeStringFromRegex = (value: string) => {
return value; return value;
} }
return specialChars.reduce((escaped, currentChar) => escaped.replace('\\' + currentChar, currentChar), value); return value.replace(specialCharUnescape, '$2');
}; };
export function stringStartsAsRegEx(str: string): boolean { export function stringStartsAsRegEx(str: string): boolean {