diff --git a/code/core/src/preview-api/modules/preview-web/parseArgsParam.test.ts b/code/core/src/preview-api/modules/preview-web/parseArgsParam.test.ts index 252b9f7618ed..f57e398bf07d 100644 --- a/code/core/src/preview-api/modules/preview-web/parseArgsParam.test.ts +++ b/code/core/src/preview-api/modules/preview-web/parseArgsParam.test.ts @@ -188,34 +188,34 @@ describe('parseArgsParam', () => { }); describe('value sanitization', () => { - it("omits values that aren't in the extended alphanumeric set", () => { - expect(parseArgsParam('key:a`b')).toStrictEqual({}); - expect(parseArgsParam('key:a~b')).toStrictEqual({}); - expect(parseArgsParam('key:a!b')).toStrictEqual({}); - expect(parseArgsParam('key:a@b')).toStrictEqual({}); - expect(parseArgsParam('key:a#b')).toStrictEqual({}); - expect(parseArgsParam('key:a$b')).toStrictEqual({}); - expect(parseArgsParam('key:a%b')).toStrictEqual({}); - expect(parseArgsParam('key:a^b')).toStrictEqual({}); - expect(parseArgsParam('key:a&b')).toStrictEqual({}); - expect(parseArgsParam('key:a*b')).toStrictEqual({}); - expect(parseArgsParam('key:a(b')).toStrictEqual({}); - expect(parseArgsParam('key:a)b')).toStrictEqual({}); - expect(parseArgsParam('key:a=b')).toStrictEqual({}); - expect(parseArgsParam('key:a[b')).toStrictEqual({}); - expect(parseArgsParam('key:a]b')).toStrictEqual({}); - expect(parseArgsParam('key:a{b')).toStrictEqual({}); - expect(parseArgsParam('key:a}b')).toStrictEqual({}); - expect(parseArgsParam('key:a\\b')).toStrictEqual({}); - expect(parseArgsParam('key:a|b')).toStrictEqual({}); - expect(parseArgsParam("key:a'b")).toStrictEqual({}); - expect(parseArgsParam('key:a"b')).toStrictEqual({}); - expect(parseArgsParam('key:a,b')).toStrictEqual({}); - expect(parseArgsParam('key:a.b')).toStrictEqual({}); + it('omits values containing structural delimiters', () => { expect(parseArgsParam('key:ab')).toStrictEqual({}); - expect(parseArgsParam('key:a/b')).toStrictEqual({}); - expect(parseArgsParam('key:a?b')).toStrictEqual({}); + expect(parseArgsParam('key:a"b')).toStrictEqual({}); + expect(parseArgsParam('key:a`b')).toStrictEqual({}); + }); + + it('allows values containing common special characters', () => { + expect(parseArgsParam('key:a~b')).toStrictEqual({ key: 'a~b' }); + expect(parseArgsParam('key:a!b')).toStrictEqual({ key: 'a!b' }); + expect(parseArgsParam('key:a@b')).toStrictEqual({ key: 'a@b' }); + expect(parseArgsParam('key:a#b')).toStrictEqual({ key: 'a#b' }); + expect(parseArgsParam('key:a$b')).toStrictEqual({ key: 'a$b' }); + expect(parseArgsParam('key:a%b')).toStrictEqual({ key: 'a%b' }); + expect(parseArgsParam('key:a^b')).toStrictEqual({ key: 'a^b' }); + expect(parseArgsParam('key:a&b')).toStrictEqual({ key: 'a&b' }); + expect(parseArgsParam('key:a*b')).toStrictEqual({ key: 'a*b' }); + expect(parseArgsParam('key:a(b')).toStrictEqual({ key: 'a(b' }); + expect(parseArgsParam('key:a)b')).toStrictEqual({ key: 'a)b' }); + expect(parseArgsParam('key:a{b')).toStrictEqual({ key: 'a{b' }); + expect(parseArgsParam('key:a}b')).toStrictEqual({ key: 'a}b' }); + expect(parseArgsParam('key:a\\b')).toStrictEqual({ key: 'a\\b' }); + expect(parseArgsParam('key:a|b')).toStrictEqual({ key: 'a|b' }); + expect(parseArgsParam("key:a'b")).toStrictEqual({ key: "a'b" }); + expect(parseArgsParam('key:a,b')).toStrictEqual({ key: 'a,b' }); + expect(parseArgsParam('key:a.b')).toStrictEqual({ key: 'a.b' }); + expect(parseArgsParam('key:a/b')).toStrictEqual({ key: 'a/b' }); + expect(parseArgsParam('key:a?b')).toStrictEqual({ key: 'a?b' }); }); it('allows values that are in the extended alphanumeric set', () => { @@ -230,23 +230,23 @@ describe('parseArgsParam', () => { expect(parseArgsParam('key:1')).toStrictEqual({ key: 1 }); expect(parseArgsParam('key:1.2')).toStrictEqual({ key: 1.2 }); expect(parseArgsParam('key:-1.2')).toStrictEqual({ key: -1.2 }); - expect(parseArgsParam('key:1.')).toStrictEqual({}); - expect(parseArgsParam('key:.2')).toStrictEqual({}); - expect(parseArgsParam('key:1.2.3')).toStrictEqual({}); + expect(parseArgsParam('key:1.')).toStrictEqual({ key: '1.' }); + expect(parseArgsParam('key:.2')).toStrictEqual({ key: '.2' }); + expect(parseArgsParam('key:1.2.3')).toStrictEqual({ key: '1.2.3' }); }); it('also applies to nested object and array values', () => { - expect(parseArgsParam('obj.key:a!b')).toStrictEqual({}); - expect(parseArgsParam('arr[0]:a!b')).toStrictEqual({}); + expect(parseArgsParam('obj.key:a { - expect(parseArgsParam('obj.key:a!b;obj.foo:val;obj.bar.baz:val')).toStrictEqual({}); - expect(parseArgsParam('obj.arr[]:a!b;obj.foo:val;obj.bar.baz:val')).toStrictEqual({}); - expect(parseArgsParam('obj.arr[0]:val;obj.arr[1]:a!b;obj.foo:val')).toStrictEqual({}); - expect(parseArgsParam('arr[]:val;arr[]:a!b;key:val')).toStrictEqual({ key: 'val' }); - expect(parseArgsParam('arr[0]:val;arr[1]:a!1;key:val')).toStrictEqual({ key: 'val' }); - expect(parseArgsParam('arr[0]:val;arr[2]:a!1;key:val')).toStrictEqual({ key: 'val' }); + expect(parseArgsParam('obj.key:a"`) and control chars in values. +// Everything else is allowed so text controls can contain special characters like / * . @ etc. +const UNSAFE_VALUE_REGEXP = /[;:<>"`\x00-\x1F\x7F]/; const NUMBER_REGEXP = /^-?[0-9]+(\.[0-9]+)?$/; const HEX_REGEXP = /^#([a-f0-9]{3,4}|[a-f0-9]{6}|[a-f0-9]{8})$/i; const COLOR_REGEXP = @@ -16,30 +19,23 @@ const validateArgs = (key = '', value: unknown): boolean => { return false; } - if (key === '' || !VALIDATION_REGEXP.test(key)) { + if (key === '' || !KEY_REGEXP.test(key)) { return false; } if (value === null || value === undefined) { - return true; - } // encoded as `!null` or `!undefined` // encoded as `!null` or `!undefined` + return true; // encoded as `!null` or `!undefined` + } - // encoded as `!null` or `!undefined` if (value instanceof Date) { - return true; - } // encoded as modified ISO string // encoded as modified ISO string + return true; // encoded as modified ISO string + } - // encoded as modified ISO string if (typeof value === 'number' || typeof value === 'boolean') { return true; } if (typeof value === 'string') { - return ( - VALIDATION_REGEXP.test(value) || - NUMBER_REGEXP.test(value) || - HEX_REGEXP.test(value) || - COLOR_REGEXP.test(value) - ); + return !UNSAFE_VALUE_REGEXP.test(value); } if (Array.isArray(value)) { diff --git a/code/core/src/router/utils.test.ts b/code/core/src/router/utils.test.ts index 9a13a877edce..f72f4ca6d04c 100644 --- a/code/core/src/router/utils.test.ts +++ b/code/core/src/router/utils.test.ts @@ -245,4 +245,52 @@ describe('buildArgsParam', () => { expect(param).toEqual('obj.nested[1].three:3'); }); }); + + describe('special characters in values', () => { + it('preserves values containing slashes', () => { + const param = buildArgsParam({}, { path: 'foo/bar/baz' }); + expect(param).toContain('path:'); + expect(param).not.toEqual(''); + }); + + it('preserves values containing dots', () => { + const param = buildArgsParam({}, { file: 'image.png' }); + expect(param).toEqual('file:image.png'); + }); + + it('preserves values containing @ symbols', () => { + const param = buildArgsParam({}, { email: 'user@example.com' }); + expect(param).toContain('email:'); + }); + + it('preserves values containing common punctuation', () => { + const param = buildArgsParam({}, { label: "Hello, world! It's great" }); + expect(param).toContain('label:'); + }); + + it('omits values containing semicolons (structural delimiter)', () => { + const param = buildArgsParam({}, { key: 'a;b' }); + expect(param).toEqual(''); + }); + + it('omits values containing colons (structural delimiter)', () => { + const param = buildArgsParam({}, { key: 'a:b' }); + expect(param).toEqual(''); + }); + + it('omits values containing angle brackets (HTML injection)', () => { + const param = buildArgsParam({}, { key: '