fix(workflow-renderer): validate dropbox host in note embed renderer (#5288)

* fix(workflow-renderer): validate dropbox host in note embed renderer

Replace the bare url.includes('dropbox.com') check with a parsed-hostname
match so attacker-controlled hosts (dropbox.com.evil.com, evil.com/?dropbox.com)
no longer get treated as direct dropbox videos. Resolves CodeQL
js/incomplete-url-substring-sanitization (#430).

* fix(workflow-renderer): rewrite dropbox embed via parsed URL, tolerate scheme-less links

Derive the direct video URL from the parsed URL object (rewrite hostname to
dl.dropboxusercontent.com for any dropbox.com/*.dropbox.com host) instead of a
www-only string replace, and accept scheme-less links. Fixes broken embeds for
m.dropbox.com / bare-host links flagged in review.
This commit is contained in:
Waleed
2026-06-30 10:11:24 -07:00
committed by GitHub
parent e7635db73a
commit 4298e577e5
@@ -17,6 +17,34 @@ function getTwitchParent(): string {
return typeof window !== 'undefined' ? window.location.hostname : 'localhost'
}
/** Parse a URL, tolerating scheme-less inputs (https is assumed). Returns null if unparseable. */
function parseUrl(url: string): URL | null {
for (const candidate of [url, `https://${url}`]) {
try {
return new URL(candidate)
} catch {}
}
return null
}
/**
* Resolve a Dropbox share link to a direct, embeddable video URL. Accepts only URLs
* whose host is `dropbox.com` or a `*.dropbox.com` subdomain (so attacker-controlled
* hosts like `dropbox.com.evil.com` are rejected), then rewrites the host to
* `dl.dropboxusercontent.com` so the file streams as media. Returns null for any
* non-Dropbox host or non-video path.
*/
function getDropboxDirectVideoUrl(url: string): string | null {
const parsed = parseUrl(url)
if (!parsed) return null
const host = parsed.hostname.toLowerCase()
if (host !== 'dropbox.com' && !host.endsWith('.dropbox.com')) return null
if (!/\.(mp4|mov|webm)$/i.test(parsed.pathname)) return null
parsed.hostname = 'dl.dropboxusercontent.com'
parsed.searchParams.delete('dl')
return parsed.toString()
}
/**
* Get embed info for supported media platforms
*/
@@ -250,11 +278,9 @@ function getEmbedInfo(url: string): EmbedInfo | null {
return { url: `https://drive.google.com/file/d/${googleDriveMatch[1]}/preview`, type: 'iframe' }
}
if (url.includes('dropbox.com') && /\.(mp4|mov|webm)/.test(url)) {
const directUrl = url
.replace('www.dropbox.com', 'dl.dropboxusercontent.com')
.replace('?dl=0', '')
return { url: directUrl, type: 'video' }
const dropboxDirectVideoUrl = getDropboxDirectVideoUrl(url)
if (dropboxDirectVideoUrl) {
return { url: dropboxDirectVideoUrl, type: 'video' }
}
const tenorMatch = url.match(/tenor\.com\/view\/[^/]+-(\d+)/)