Skip to content

Commit 922a274

Browse files
committed
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.
1 parent 7662ecc commit 922a274

2 files changed

Lines changed: 36 additions & 14 deletions

File tree

packages/utils/src/media-embed.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,25 @@ describe('getEmbedInfo', () => {
77
expect(getEmbedInfo('https://www.youtube.com/watch?v=dQw4w9WgXcQ')).toEqual(expected)
88
expect(getEmbedInfo('https://youtu.be/dQw4w9WgXcQ')).toEqual(expected)
99
expect(getEmbedInfo('https://www.youtube.com/embed/dQw4w9WgXcQ')).toEqual(expected)
10+
// Extra query params around v= still resolve.
11+
expect(getEmbedInfo('https://www.youtube.com/watch?list=RD&v=dQw4w9WgXcQ&t=5')).toEqual(
12+
expected
13+
)
14+
})
15+
16+
it('maps Facebook and fb.watch video links to the video plugin', () => {
17+
expect(getEmbedInfo('https://www.facebook.com/some.page/videos/1234567890')).toEqual({
18+
url: 'https://www.facebook.com/plugins/video.php?href=https%3A%2F%2Fwww.facebook.com%2Fsome.page%2Fvideos%2F1234567890&show_text=false',
19+
type: 'iframe',
20+
})
21+
expect(getEmbedInfo('https://fb.watch/abc123')?.type).toBe('iframe')
22+
expect(getEmbedInfo('https://www.facebook.com/some.page/about')).toBeNull()
23+
})
24+
25+
it('extracts the Giphy id from the trailing slug token', () => {
26+
const expected = { url: 'https://giphy.com/embed/abc123', type: 'iframe', aspectRatio: '1/1' }
27+
expect(getEmbedInfo('https://giphy.com/gifs/funny-cat-abc123')).toEqual(expected)
28+
expect(getEmbedInfo('https://giphy.com/embed/abc123')).toEqual(expected)
1029
})
1130

1231
it('maps Vimeo and Spotify URLs with their aspect ratios', () => {

packages/utils/src/media-embed.ts

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,12 @@ function toDropboxDirectVideoUrl(parsed: URL): string | null {
6464
export function getEmbedInfo(url: string): EmbedInfo | null {
6565
const parsed = parseUrl(url)
6666
const host = parsed?.hostname.toLowerCase() ?? null
67-
if (hostMatches(host, 'youtube.com', 'youtu.be')) {
68-
const youtubeMatch = url.match(
69-
/(?:youtube\.com\/watch\?(?:.*&)?v=|youtu\.be\/|youtube\.com\/embed\/)([a-zA-Z0-9_-]{11})/
70-
)
71-
if (youtubeMatch) {
72-
return { url: `https://www.youtube.com/embed/${youtubeMatch[1]}`, type: 'iframe' }
67+
if (parsed && hostMatches(host, 'youtube.com', 'youtu.be')) {
68+
const id = hostMatches(host, 'youtu.be')
69+
? parsed.pathname.slice(1)
70+
: (parsed.searchParams.get('v') ?? parsed.pathname.match(/^\/embed\/([^/?]+)/)?.[1])
71+
if (id && /^[a-zA-Z0-9_-]{11}$/.test(id)) {
72+
return { url: `https://www.youtube.com/embed/${id}`, type: 'iframe' }
7373
}
7474
}
7575

@@ -209,10 +209,11 @@ export function getEmbedInfo(url: string): EmbedInfo | null {
209209
}
210210
}
211211

212-
if (hostMatches(host, 'facebook.com', 'fb.watch')) {
213-
const facebookVideoMatch =
214-
url.match(/facebook\.com\/.*\/videos\/(\d+)/) || url.match(/fb\.watch\/([a-zA-Z0-9_-]+)/)
215-
if (facebookVideoMatch) {
212+
if (parsed && hostMatches(host, 'facebook.com', 'fb.watch')) {
213+
const isFacebookVideo = hostMatches(host, 'fb.watch')
214+
? /^\/[a-zA-Z0-9_-]+/.test(parsed.pathname)
215+
: /\/videos\/\d+/.test(parsed.pathname)
216+
if (isFacebookVideo) {
216217
return {
217218
url: `https://www.facebook.com/plugins/video.php?href=${encodeURIComponent(url)}&show_text=false`,
218219
type: 'iframe',
@@ -320,10 +321,12 @@ export function getEmbedInfo(url: string): EmbedInfo | null {
320321
}
321322
}
322323

323-
if (hostMatches(host, 'giphy.com')) {
324-
const giphyMatch = url.match(/giphy\.com\/(?:gifs|embed)\/(?:.*-)?([a-zA-Z0-9]+)/)
325-
if (giphyMatch) {
326-
return { url: `https://giphy.com/embed/${giphyMatch[1]}`, type: 'iframe', aspectRatio: '1/1' }
324+
if (parsed && hostMatches(host, 'giphy.com')) {
325+
// Giphy ids are the trailing hyphen-delimited token of a /gifs/ or /embed/ path segment.
326+
const segment = parsed.pathname.match(/^\/(?:gifs|embed)\/([^/]+)/)?.[1]
327+
const giphyId = segment?.split('-').pop()
328+
if (giphyId && /^[a-zA-Z0-9]+$/.test(giphyId)) {
329+
return { url: `https://giphy.com/embed/${giphyId}`, type: 'iframe', aspectRatio: '1/1' }
327330
}
328331
}
329332

0 commit comments

Comments
 (0)