Skip to content

Commit 4298e57

Browse files
authored
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.
1 parent e7635db commit 4298e57

1 file changed

Lines changed: 31 additions & 5 deletions

File tree

packages/workflow-renderer/src/note/note-block-view.tsx

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,34 @@ function getTwitchParent(): string {
1717
return typeof window !== 'undefined' ? window.location.hostname : 'localhost'
1818
}
1919

20+
/** Parse a URL, tolerating scheme-less inputs (https is assumed). Returns null if unparseable. */
21+
function parseUrl(url: string): URL | null {
22+
for (const candidate of [url, `https://${url}`]) {
23+
try {
24+
return new URL(candidate)
25+
} catch {}
26+
}
27+
return null
28+
}
29+
30+
/**
31+
* Resolve a Dropbox share link to a direct, embeddable video URL. Accepts only URLs
32+
* whose host is `dropbox.com` or a `*.dropbox.com` subdomain (so attacker-controlled
33+
* hosts like `dropbox.com.evil.com` are rejected), then rewrites the host to
34+
* `dl.dropboxusercontent.com` so the file streams as media. Returns null for any
35+
* non-Dropbox host or non-video path.
36+
*/
37+
function getDropboxDirectVideoUrl(url: string): string | null {
38+
const parsed = parseUrl(url)
39+
if (!parsed) return null
40+
const host = parsed.hostname.toLowerCase()
41+
if (host !== 'dropbox.com' && !host.endsWith('.dropbox.com')) return null
42+
if (!/\.(mp4|mov|webm)$/i.test(parsed.pathname)) return null
43+
parsed.hostname = 'dl.dropboxusercontent.com'
44+
parsed.searchParams.delete('dl')
45+
return parsed.toString()
46+
}
47+
2048
/**
2149
* Get embed info for supported media platforms
2250
*/
@@ -250,11 +278,9 @@ function getEmbedInfo(url: string): EmbedInfo | null {
250278
return { url: `https://drive.google.com/file/d/${googleDriveMatch[1]}/preview`, type: 'iframe' }
251279
}
252280

253-
if (url.includes('dropbox.com') && /\.(mp4|mov|webm)/.test(url)) {
254-
const directUrl = url
255-
.replace('www.dropbox.com', 'dl.dropboxusercontent.com')
256-
.replace('?dl=0', '')
257-
return { url: directUrl, type: 'video' }
281+
const dropboxDirectVideoUrl = getDropboxDirectVideoUrl(url)
282+
if (dropboxDirectVideoUrl) {
283+
return { url: dropboxDirectVideoUrl, type: 'video' }
258284
}
259285

260286
const tenorMatch = url.match(/tenor\.com\/view\/[^/]+-(\d+)/)

0 commit comments

Comments
 (0)