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;
}