diff --git a/change/@fluentui-react-charting-355a59ec-794a-4da2-93a6-ea28e5c1c204.json b/change/@fluentui-react-charting-355a59ec-794a-4da2-93a6-ea28e5c1c204.json new file mode 100644 index 00000000000000..fee447e608b149 --- /dev/null +++ b/change/@fluentui-react-charting-355a59ec-794a-4da2-93a6-ea28e5c1c204.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "safeurl related bug fix", + "packageName": "@fluentui/react-charting", + "email": "132879294+v-baambati@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-charts-eede23c0-c45d-4f45-9a22-a7a9334860bb.json b/change/@fluentui-react-charts-eede23c0-c45d-4f45-9a22-a7a9334860bb.json new file mode 100644 index 00000000000000..f1d8f42a9a98e3 --- /dev/null +++ b/change/@fluentui-react-charts-eede23c0-c45d-4f45-9a22-a7a9334860bb.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "safeurl related bug fix", + "packageName": "@fluentui/react-charts", + "email": "132879294+v-baambati@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/packages/charts/react-charting/src/utilities/UtilityUnitTests.test.ts b/packages/charts/react-charting/src/utilities/UtilityUnitTests.test.ts index 8cc45358358c6c..fc170e02ad09df 100644 --- a/packages/charts/react-charting/src/utilities/UtilityUnitTests.test.ts +++ b/packages/charts/react-charting/src/utilities/UtilityUnitTests.test.ts @@ -1523,6 +1523,10 @@ describe('isSafeUrl', () => { expect(utils.isSafeUrl('https://example.com')).toBe(true); }); + test('Should allow https URL with leading whitespace', () => { + expect(utils.isSafeUrl(' https://example.com')).toBe(true); + }); + test('Should allow https URL with path, query, and fragment', () => { expect(utils.isSafeUrl('https://example.com/path?q=1#section')).toBe(true); }); @@ -1552,6 +1556,26 @@ describe('isSafeUrl', () => { expect(utils.isSafeUrl('javascript:alert(1)')).toBe(false); }); + test('Should block javascript: protocol with leading whitespace', () => { + // eslint-disable-next-line no-script-url + expect(utils.isSafeUrl(' javascript:alert(1)')).toBe(false); + }); + + test('Should block javascript: protocol with leading tabs/newlines', () => { + // eslint-disable-next-line no-script-url + expect(utils.isSafeUrl('\n\tjavascript:alert(1)')).toBe(false); + }); + + test('Should block javascript: protocol with embedded newline in scheme', () => { + // eslint-disable-next-line no-script-url + expect(utils.isSafeUrl('java\nscript:alert(1)')).toBe(false); + }); + + test('Should block javascript: protocol with embedded tab in scheme', () => { + // eslint-disable-next-line no-script-url + expect(utils.isSafeUrl('java\tscript:alert(1)')).toBe(false); + }); + test('Should block data: protocol', () => { expect(utils.isSafeUrl('data:text/html,')).toBe(false); }); @@ -1560,6 +1584,25 @@ describe('isSafeUrl', () => { expect(utils.isSafeUrl('vbscript:msgbox("xss")')).toBe(false); }); + test('Should block vbscript:alert(1)', () => { + expect(utils.isSafeUrl('vbscript:alert(1)')).toBe(false); + }); + + test('Should block javascript: protocol with null byte prefix', () => { + // eslint-disable-next-line no-script-url + expect(utils.isSafeUrl('\0javascript:alert(1)')).toBe(false); + }); + + test('Should block mixed-case javascript: protocol with embedded newline', () => { + // eslint-disable-next-line no-script-url + expect(utils.isSafeUrl('JaVa\nScRiPt:alert(1)')).toBe(false); + }); + + test('Should block javascript: protocol with CRLF prefix', () => { + // eslint-disable-next-line no-script-url + expect(utils.isSafeUrl('\r\njavascript:alert(1)')).toBe(false); + }); + test('Should block file: protocol', () => { expect(utils.isSafeUrl('file:///etc/passwd')).toBe(false); }); @@ -1580,6 +1623,10 @@ describe('isSafeUrl', () => { expect(utils.isSafeUrl('custom:payload')).toBe(false); }); + test('Should block custom: protocol with leading whitespace', () => { + expect(utils.isSafeUrl(' custom:payload')).toBe(false); + }); + test('Should allow a path that contains a colon but is not a scheme', () => { expect(utils.isSafeUrl('/path/to:resource')).toBe(true); }); diff --git a/packages/charts/react-charting/src/utilities/utilities.ts b/packages/charts/react-charting/src/utilities/utilities.ts index eeae65c4af60bf..262e00eaf7a07e 100644 --- a/packages/charts/react-charting/src/utilities/utilities.ts +++ b/packages/charts/react-charting/src/utilities/utilities.ts @@ -2556,9 +2556,9 @@ const truncateTextToFitWidth = (text: string, maxWidth: number, measure: (s: str }; export function isSafeUrl(href: string): boolean { - if (/^[a-z][a-z0-9+.-]*:/i.test(href)) { - return /^(https?|mailto|tel|ftp):/i.test(href); + const normalized = href.replace(/[\u0000-\u001F\u007F\s]+/g, ''); + if (/^[a-z][a-z0-9+.-]*:/i.test(normalized)) { + return /^(https?|mailto|tel|ftp):/i.test(normalized); } - return true; } diff --git a/packages/charts/react-charts/library/src/utilities/UtilityUnitTests.test.ts b/packages/charts/react-charts/library/src/utilities/UtilityUnitTests.test.ts index 2237bf11890148..195f1c3820efee 100644 --- a/packages/charts/react-charts/library/src/utilities/UtilityUnitTests.test.ts +++ b/packages/charts/react-charts/library/src/utilities/UtilityUnitTests.test.ts @@ -1528,6 +1528,10 @@ describe('isSafeUrl', () => { expect(utils.isSafeUrl('https://example.com')).toBe(true); }); + test('Should allow https URL with leading whitespace', () => { + expect(utils.isSafeUrl(' https://example.com')).toBe(true); + }); + test('Should allow https URL with path, query, and fragment', () => { expect(utils.isSafeUrl('https://example.com/path?q=1#section')).toBe(true); }); @@ -1557,6 +1561,26 @@ describe('isSafeUrl', () => { expect(utils.isSafeUrl('javascript:alert(1)')).toBe(false); }); + test('Should block javascript: protocol with leading whitespace', () => { + // eslint-disable-next-line no-script-url + expect(utils.isSafeUrl(' javascript:alert(1)')).toBe(false); + }); + + test('Should block javascript: protocol with leading tabs/newlines', () => { + // eslint-disable-next-line no-script-url + expect(utils.isSafeUrl('\n\tjavascript:alert(1)')).toBe(false); + }); + + test('Should block javascript: protocol with embedded newline in scheme', () => { + // eslint-disable-next-line no-script-url + expect(utils.isSafeUrl('java\nscript:alert(1)')).toBe(false); + }); + + test('Should block javascript: protocol with embedded tab in scheme', () => { + // eslint-disable-next-line no-script-url + expect(utils.isSafeUrl('java\tscript:alert(1)')).toBe(false); + }); + test('Should block data: protocol', () => { expect(utils.isSafeUrl('data:text/html,')).toBe(false); }); @@ -1565,6 +1589,25 @@ describe('isSafeUrl', () => { expect(utils.isSafeUrl('vbscript:msgbox("xss")')).toBe(false); }); + test('Should block vbscript:alert(1)', () => { + expect(utils.isSafeUrl('vbscript:alert(1)')).toBe(false); + }); + + test('Should block javascript: protocol with null byte prefix', () => { + // eslint-disable-next-line no-script-url + expect(utils.isSafeUrl('\0javascript:alert(1)')).toBe(false); + }); + + test('Should block mixed-case javascript: protocol with embedded newline', () => { + // eslint-disable-next-line no-script-url + expect(utils.isSafeUrl('JaVa\nScRiPt:alert(1)')).toBe(false); + }); + + test('Should block javascript: protocol with CRLF prefix', () => { + // eslint-disable-next-line no-script-url + expect(utils.isSafeUrl('\r\njavascript:alert(1)')).toBe(false); + }); + test('Should block file: protocol', () => { expect(utils.isSafeUrl('file:///etc/passwd')).toBe(false); }); @@ -1585,6 +1628,10 @@ describe('isSafeUrl', () => { expect(utils.isSafeUrl('custom:payload')).toBe(false); }); + test('Should block custom: protocol with leading whitespace', () => { + expect(utils.isSafeUrl(' custom:payload')).toBe(false); + }); + test('Should allow a path that contains a colon but is not a scheme', () => { expect(utils.isSafeUrl('/path/to:resource')).toBe(true); }); diff --git a/packages/charts/react-charts/library/src/utilities/utilities.ts b/packages/charts/react-charts/library/src/utilities/utilities.ts index 4d2249185fcadb..add89c7e9ceaa9 100644 --- a/packages/charts/react-charts/library/src/utilities/utilities.ts +++ b/packages/charts/react-charts/library/src/utilities/utilities.ts @@ -2716,8 +2716,9 @@ const truncateTextToFitWidth = (text: string, maxWidth: number, measure: (s: str }; export function isSafeUrl(href: string): boolean { - if (/^[a-z][a-z0-9+.-]*:/i.test(href)) { - return /^(https?|mailto|tel|ftp):/i.test(href); + const normalized = href.replace(/[\u0000-\u001F\u007F\s]+/g, ''); + if (/^[a-z][a-z0-9+.-]*:/i.test(normalized)) { + return /^(https?|mailto|tel|ftp):/i.test(normalized); } return true; }