From 922a274f6210fe032d478520e912e39d1e96eb3f Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 30 Jun 2026 18:00:37 -0700 Subject: [PATCH 1/3] fix(media-embed): remove ReDoS-prone regexes in host-gated providers Replace the unbounded '.*' patterns flagged by CodeQL (js/polynomial-redos) in the YouTube, Facebook, and Giphy branches with bounded extraction off the parsed URL (pathname / searchParams). Eliminates the O(n^2) backtracking a crafted valid-host URL could trigger, with no change to matched links. --- packages/utils/src/media-embed.test.ts | 19 ++++++++++++++++ packages/utils/src/media-embed.ts | 31 ++++++++++++++------------ 2 files changed, 36 insertions(+), 14 deletions(-) diff --git a/packages/utils/src/media-embed.test.ts b/packages/utils/src/media-embed.test.ts index 6422ebfa5c0..04cf0d61445 100644 --- a/packages/utils/src/media-embed.test.ts +++ b/packages/utils/src/media-embed.test.ts @@ -7,6 +7,25 @@ describe('getEmbedInfo', () => { expect(getEmbedInfo('https://www.youtube.com/watch?v=dQw4w9WgXcQ')).toEqual(expected) expect(getEmbedInfo('https://youtu.be/dQw4w9WgXcQ')).toEqual(expected) expect(getEmbedInfo('https://www.youtube.com/embed/dQw4w9WgXcQ')).toEqual(expected) + // Extra query params around v= still resolve. + expect(getEmbedInfo('https://www.youtube.com/watch?list=RD&v=dQw4w9WgXcQ&t=5')).toEqual( + expected + ) + }) + + it('maps Facebook and fb.watch video links to the video plugin', () => { + expect(getEmbedInfo('https://www.facebook.com/some.page/videos/1234567890')).toEqual({ + url: 'https://www.facebook.com/plugins/video.php?href=https%3A%2F%2Fwww.facebook.com%2Fsome.page%2Fvideos%2F1234567890&show_text=false', + type: 'iframe', + }) + expect(getEmbedInfo('https://fb.watch/abc123')?.type).toBe('iframe') + expect(getEmbedInfo('https://www.facebook.com/some.page/about')).toBeNull() + }) + + it('extracts the Giphy id from the trailing slug token', () => { + const expected = { url: 'https://giphy.com/embed/abc123', type: 'iframe', aspectRatio: '1/1' } + expect(getEmbedInfo('https://giphy.com/gifs/funny-cat-abc123')).toEqual(expected) + expect(getEmbedInfo('https://giphy.com/embed/abc123')).toEqual(expected) }) it('maps Vimeo and Spotify URLs with their aspect ratios', () => { diff --git a/packages/utils/src/media-embed.ts b/packages/utils/src/media-embed.ts index 1432e22198d..b823cdd6ffb 100644 --- a/packages/utils/src/media-embed.ts +++ b/packages/utils/src/media-embed.ts @@ -64,12 +64,12 @@ function toDropboxDirectVideoUrl(parsed: URL): string | null { export function getEmbedInfo(url: string): EmbedInfo | null { const parsed = parseUrl(url) const host = parsed?.hostname.toLowerCase() ?? null - if (hostMatches(host, 'youtube.com', 'youtu.be')) { - const youtubeMatch = url.match( - /(?:youtube\.com\/watch\?(?:.*&)?v=|youtu\.be\/|youtube\.com\/embed\/)([a-zA-Z0-9_-]{11})/ - ) - if (youtubeMatch) { - return { url: `https://www.youtube.com/embed/${youtubeMatch[1]}`, type: 'iframe' } + if (parsed && hostMatches(host, 'youtube.com', 'youtu.be')) { + const id = hostMatches(host, 'youtu.be') + ? parsed.pathname.slice(1) + : (parsed.searchParams.get('v') ?? parsed.pathname.match(/^\/embed\/([^/?]+)/)?.[1]) + if (id && /^[a-zA-Z0-9_-]{11}$/.test(id)) { + return { url: `https://www.youtube.com/embed/${id}`, type: 'iframe' } } } @@ -209,10 +209,11 @@ export function getEmbedInfo(url: string): EmbedInfo | null { } } - if (hostMatches(host, 'facebook.com', 'fb.watch')) { - const facebookVideoMatch = - url.match(/facebook\.com\/.*\/videos\/(\d+)/) || url.match(/fb\.watch\/([a-zA-Z0-9_-]+)/) - if (facebookVideoMatch) { + if (parsed && hostMatches(host, 'facebook.com', 'fb.watch')) { + const isFacebookVideo = hostMatches(host, 'fb.watch') + ? /^\/[a-zA-Z0-9_-]+/.test(parsed.pathname) + : /\/videos\/\d+/.test(parsed.pathname) + if (isFacebookVideo) { return { url: `https://www.facebook.com/plugins/video.php?href=${encodeURIComponent(url)}&show_text=false`, type: 'iframe', @@ -320,10 +321,12 @@ export function getEmbedInfo(url: string): EmbedInfo | null { } } - if (hostMatches(host, 'giphy.com')) { - const giphyMatch = url.match(/giphy\.com\/(?:gifs|embed)\/(?:.*-)?([a-zA-Z0-9]+)/) - if (giphyMatch) { - return { url: `https://giphy.com/embed/${giphyMatch[1]}`, type: 'iframe', aspectRatio: '1/1' } + if (parsed && hostMatches(host, 'giphy.com')) { + // Giphy ids are the trailing hyphen-delimited token of a /gifs/ or /embed/ path segment. + const segment = parsed.pathname.match(/^\/(?:gifs|embed)\/([^/]+)/)?.[1] + const giphyId = segment?.split('-').pop() + if (giphyId && /^[a-zA-Z0-9]+$/.test(giphyId)) { + return { url: `https://giphy.com/embed/${giphyId}`, type: 'iframe', aspectRatio: '1/1' } } } From f3e4d4b12359c63bbc641ab9b0c36b9126314311 Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 30 Jun 2026 18:07:51 -0700 Subject: [PATCH 2/3] test(media-embed): lock youtu.be trailing-slash + edge parity Use the first path segment for youtu.be ids so a trailing slash still resolves (matching the previous regex), and cover extra-query-param, si-param, embed-query, and short-id cases. --- packages/utils/src/media-embed.test.ts | 8 +++++++- packages/utils/src/media-embed.ts | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/utils/src/media-embed.test.ts b/packages/utils/src/media-embed.test.ts index 04cf0d61445..02f417f57b2 100644 --- a/packages/utils/src/media-embed.test.ts +++ b/packages/utils/src/media-embed.test.ts @@ -7,10 +7,16 @@ describe('getEmbedInfo', () => { expect(getEmbedInfo('https://www.youtube.com/watch?v=dQw4w9WgXcQ')).toEqual(expected) expect(getEmbedInfo('https://youtu.be/dQw4w9WgXcQ')).toEqual(expected) expect(getEmbedInfo('https://www.youtube.com/embed/dQw4w9WgXcQ')).toEqual(expected) - // Extra query params around v= still resolve. + // Extra query params around v=, a youtu.be tracking param, a trailing slash, + // and an embed query string all still resolve to the same embed. expect(getEmbedInfo('https://www.youtube.com/watch?list=RD&v=dQw4w9WgXcQ&t=5')).toEqual( expected ) + expect(getEmbedInfo('https://youtu.be/dQw4w9WgXcQ?si=abc')).toEqual(expected) + expect(getEmbedInfo('https://youtu.be/dQw4w9WgXcQ/')).toEqual(expected) + expect(getEmbedInfo('https://www.youtube.com/embed/dQw4w9WgXcQ?rel=0')).toEqual(expected) + // A non-11-char id is not a valid YouTube video and does not embed. + expect(getEmbedInfo('https://www.youtube.com/watch?v=short')).toBeNull() }) it('maps Facebook and fb.watch video links to the video plugin', () => { diff --git a/packages/utils/src/media-embed.ts b/packages/utils/src/media-embed.ts index b823cdd6ffb..d89aa805772 100644 --- a/packages/utils/src/media-embed.ts +++ b/packages/utils/src/media-embed.ts @@ -66,7 +66,7 @@ export function getEmbedInfo(url: string): EmbedInfo | null { const host = parsed?.hostname.toLowerCase() ?? null if (parsed && hostMatches(host, 'youtube.com', 'youtu.be')) { const id = hostMatches(host, 'youtu.be') - ? parsed.pathname.slice(1) + ? parsed.pathname.split('/')[1] : (parsed.searchParams.get('v') ?? parsed.pathname.match(/^\/embed\/([^/?]+)/)?.[1]) if (id && /^[a-zA-Z0-9_-]{11}$/.test(id)) { return { url: `https://www.youtube.com/embed/${id}`, type: 'iframe' } From 64df33e4cb88611d35342c51106a563fb1d6d239 Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 30 Jun 2026 18:10:40 -0700 Subject: [PATCH 3/3] fix(media-embed): dispatch YouTube id by path shape; drop inline comments - Resolve id from the /embed/ path segment before the ?v= query param so a valid embed URL with a spurious v param still embeds (was returning null) - Remove non-TSDoc inline comments from the module and its test --- packages/utils/src/media-embed.test.ts | 9 +-------- packages/utils/src/media-embed.ts | 9 +++++---- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/packages/utils/src/media-embed.test.ts b/packages/utils/src/media-embed.test.ts index 02f417f57b2..3ff64c482b3 100644 --- a/packages/utils/src/media-embed.test.ts +++ b/packages/utils/src/media-embed.test.ts @@ -7,15 +7,13 @@ describe('getEmbedInfo', () => { expect(getEmbedInfo('https://www.youtube.com/watch?v=dQw4w9WgXcQ')).toEqual(expected) expect(getEmbedInfo('https://youtu.be/dQw4w9WgXcQ')).toEqual(expected) expect(getEmbedInfo('https://www.youtube.com/embed/dQw4w9WgXcQ')).toEqual(expected) - // Extra query params around v=, a youtu.be tracking param, a trailing slash, - // and an embed query string all still resolve to the same embed. expect(getEmbedInfo('https://www.youtube.com/watch?list=RD&v=dQw4w9WgXcQ&t=5')).toEqual( expected ) expect(getEmbedInfo('https://youtu.be/dQw4w9WgXcQ?si=abc')).toEqual(expected) expect(getEmbedInfo('https://youtu.be/dQw4w9WgXcQ/')).toEqual(expected) expect(getEmbedInfo('https://www.youtube.com/embed/dQw4w9WgXcQ?rel=0')).toEqual(expected) - // A non-11-char id is not a valid YouTube video and does not embed. + expect(getEmbedInfo('https://www.youtube.com/embed/dQw4w9WgXcQ?v=notAnId')).toEqual(expected) expect(getEmbedInfo('https://www.youtube.com/watch?v=short')).toBeNull() }) @@ -63,13 +61,10 @@ describe('getEmbedInfo', () => { }) it('only embeds when the parsed host belongs to the provider', () => { - // A provider domain in the path or as a subdomain prefix of an attacker host - // must not be treated as that provider. expect(getEmbedInfo('https://evil.com/youtube.com/watch?v=dQw4w9WgXcQ')).toBeNull() expect(getEmbedInfo('https://youtube.com.evil.com/watch?v=dQw4w9WgXcQ')).toBeNull() expect(getEmbedInfo('https://evil.com/open.spotify.com/track/abc123')).toBeNull() expect(getEmbedInfo('https://vimeo.com.evil.com/123456')).toBeNull() - // Legitimate subdomains of a provider still embed. expect(getEmbedInfo('https://m.youtube.com/watch?v=dQw4w9WgXcQ')).toEqual({ url: 'https://www.youtube.com/embed/dQw4w9WgXcQ', type: 'iframe', @@ -96,8 +91,6 @@ describe('getEmbedInfo', () => { }) it('does not apply the Dropbox direct-link rewrite to look-alike hosts', () => { - // Look-alike hosts fall through to the generic video handler with their - // original (untrusted) host intact — never rewritten as if trusted Dropbox. expect(getEmbedInfo('https://dropbox.com.evil.com/clip.mp4')?.url).not.toContain( 'dropboxusercontent.com' ) diff --git a/packages/utils/src/media-embed.ts b/packages/utils/src/media-embed.ts index d89aa805772..b5aaa4068fb 100644 --- a/packages/utils/src/media-embed.ts +++ b/packages/utils/src/media-embed.ts @@ -65,9 +65,11 @@ export function getEmbedInfo(url: string): EmbedInfo | null { const parsed = parseUrl(url) const host = parsed?.hostname.toLowerCase() ?? null if (parsed && hostMatches(host, 'youtube.com', 'youtu.be')) { - const id = hostMatches(host, 'youtu.be') - ? parsed.pathname.split('/')[1] - : (parsed.searchParams.get('v') ?? parsed.pathname.match(/^\/embed\/([^/?]+)/)?.[1]) + const segments = parsed.pathname.split('/') + let id: string | null | undefined + if (hostMatches(host, 'youtu.be')) id = segments[1] + else if (segments[1] === 'embed') id = segments[2] + else id = parsed.searchParams.get('v') if (id && /^[a-zA-Z0-9_-]{11}$/.test(id)) { return { url: `https://www.youtube.com/embed/${id}`, type: 'iframe' } } @@ -322,7 +324,6 @@ export function getEmbedInfo(url: string): EmbedInfo | null { } if (parsed && hostMatches(host, 'giphy.com')) { - // Giphy ids are the trailing hyphen-delimited token of a /gifs/ or /embed/ path segment. const segment = parsed.pathname.match(/^\/(?:gifs|embed)\/([^/]+)/)?.[1] const giphyId = segment?.split('-').pop() if (giphyId && /^[a-zA-Z0-9]+$/.test(giphyId)) {