mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
fix(media-embed): remove ReDoS-prone regexes in host-gated providers (#5305)
* 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. * 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. * 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
This commit is contained in:
@@ -7,6 +7,29 @@ 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)
|
||||
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)
|
||||
expect(getEmbedInfo('https://www.youtube.com/embed/dQw4w9WgXcQ?v=notAnId')).toEqual(expected)
|
||||
expect(getEmbedInfo('https://www.youtube.com/watch?v=short')).toBeNull()
|
||||
})
|
||||
|
||||
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', () => {
|
||||
@@ -38,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',
|
||||
@@ -71,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'
|
||||
)
|
||||
|
||||
@@ -64,12 +64,14 @@ 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 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' }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,10 +211,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 +323,11 @@ 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')) {
|
||||
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' }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user