MM-67387 Prevent useExternalLink from erroring on certain invalid links (#35393)

* MM-67387 Prevent useExternalLink from erroring on certain invalid links

* Make check for domain in useExternalLink stricter

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
This commit is contained in:
Harrison Healey
2026-03-11 16:32:35 -04:00
committed by GitHub
co-authored by Mattermost Build
parent 162ed1bacd
commit baf2bcb6f5
2 changed files with 44 additions and 2 deletions
@@ -136,9 +136,31 @@ describe('useExternalLink', () => {
expect(firstHref).toBe(secondHref);
expect(firstParams).toBe(secondParams);
});
it('do not substitute %20 on query params', () => {
const url = 'https://www.mattermost.com/some/url?subject=hello%20world';
const {result: {current: [href]}} = renderHookWithContext(() => useExternalLink(url), getBaseState());
expect(href).toContain('subject=hello%20world');
});
it('do not error on invalid URLs', () => {
const invalidUrl = 'not a valid url';
const {result: {current: [href, queryParams]}} = renderHookWithContext(() => useExternalLink(invalidUrl), getBaseState());
expect(href).toBe(invalidUrl);
expect(queryParams).toEqual({});
});
it('do not modify arbitrary links that happen to include mattermost.com', () => {
const invalidUrl = 'https://example.com/mattermost.com';
const {result: {current: [href, queryParams]}} = renderHookWithContext(() => useExternalLink(invalidUrl), getBaseState());
expect(href).toBe(invalidUrl);
expect(queryParams).toEqual({});
});
it('do not modify mailto links on mattermost.com', () => {
const mailtoUrl = 'mailto:support@mattermost.com';
const {result: {current: [href, queryParams]}} = renderHookWithContext(() => useExternalLink(mailtoUrl), getBaseState());
expect(href).toBe(mailtoUrl);
expect(queryParams).toEqual({});
});
});
@@ -17,6 +17,17 @@ export type ExternalLinkQueryParams = {
userId?: string;
}
/**
* useExternalLink is used when linking outside of the MM server to add extra tracking parameters when linking to any
* page on mattermost.com (such as our docs or marketing websites). When passed any URL that isn't on mattermost.com,
* it returns the original URL unmodified.
*
* @param href The external URL being linked to
* @param location The location of the link within the app
* @param overwriteQueryParams
* @return {[string, Record<string, string>]} A tuple containing the URL (whether or not it was modified) and all query
* parameters on that link (either pre-existing or added by this hook)
*/
export function useExternalLink(href: string, location: string = '', overwriteQueryParams: ExternalLinkQueryParams = {}): [string, Record<string, string>] {
const userId = useSelector(getCurrentUserId);
const config = useSelector(getConfig);
@@ -28,11 +39,20 @@ export function useExternalLink(href: string, location: string = '', overwriteQu
const isCloud = useSelector((state: GlobalState) => getLicense(state)?.Cloud === 'true');
return useMemo(() => {
if (!href?.includes('mattermost.com') || href?.startsWith('mailto:')) {
let parsedUrl;
try {
parsedUrl = new URL(href);
} catch {
return [href, {}];
}
const parsedUrl = new URL(href);
if (parsedUrl.hostname !== 'mattermost.com' && !parsedUrl.hostname.endsWith('.mattermost.com')) {
return [href, {}];
}
if (parsedUrl.protocol === 'mailto:') {
return [href, {}];
}
// Determine edition type (enterprise vs team)
const isEnterpriseReady = config?.BuildEnterpriseReady === 'true';