From 988f81911ca58e3ca2583e0dd44a954dd00e09d0 Mon Sep 17 00:00:00 2001 From: David Luzar <5153846+dwelle@users.noreply.github.com> Date: Sat, 13 Apr 2024 18:51:30 +0200 Subject: [PATCH] fix: allow same origin for all necessary domains (#7889) --- src/element/embeddable.ts | 105 ++++++++++++++++++++++++++++---------- 1 file changed, 78 insertions(+), 27 deletions(-) diff --git a/src/element/embeddable.ts b/src/element/embeddable.ts index 0010246fbb..6c73006ad3 100644 --- a/src/element/embeddable.ts +++ b/src/element/embeddable.ts @@ -19,7 +19,7 @@ type EmbeddedLink = | ({ aspectRatio: { w: number; h: number }; warning?: string; - sandbox?: { allowSameOrigin?: boolean }; + sandbox: { allowSameOrigin?: boolean }; } & ( | { type: "video" | "generic"; link: string } | { type: "document"; srcdoc: (theme: Theme) => string } @@ -67,7 +67,18 @@ const ALLOWED_DOMAINS = new Set([ "stackblitz.com", "val.town", "giphy.com", - "dddice.com", +]); + +const ALLOW_SAME_ORIGIN = new Set([ + "youtube.com", + "youtu.be", + "vimeo.com", + "player.vimeo.com", + "figma.com", + "twitter.com", + "x.com", + "*.simplepdf.eu", + "stackblitz.com", ]); const createSrcDoc = (body: string) => { @@ -85,6 +96,10 @@ export const getEmbedLink = (link: string | null | undefined): EmbeddedLink => { const originalLink = link; + const allowSameOrigin = ALLOW_SAME_ORIGIN.has( + matchHostname(link, ALLOW_SAME_ORIGIN) || "", + ); + let type: "video" | "generic" = "generic"; let aspectRatio = { w: 560, h: 840 }; const ytLink = link.match(RE_YOUTUBE); @@ -107,8 +122,18 @@ export const getEmbedLink = (link: string | null | undefined): EmbeddedLink => { break; } aspectRatio = isPortrait ? { w: 315, h: 560 } : { w: 560, h: 315 }; - embeddedLinkCache.set(originalLink, { link, aspectRatio, type }); - return { link, aspectRatio, type }; + embeddedLinkCache.set(originalLink, { + link, + aspectRatio, + type, + sandbox: { allowSameOrigin }, + }); + return { + link, + aspectRatio, + type, + sandbox: { allowSameOrigin }, + }; } const vimeoLink = link.match(RE_VIMEO); @@ -122,8 +147,13 @@ export const getEmbedLink = (link: string | null | undefined): EmbeddedLink => { aspectRatio = { w: 560, h: 315 }; //warning deliberately ommited so it is displayed only once per link //same link next time will be served from cache - embeddedLinkCache.set(originalLink, { link, aspectRatio, type }); - return { link, aspectRatio, type, warning }; + embeddedLinkCache.set(originalLink, { + link, + aspectRatio, + type, + sandbox: { allowSameOrigin }, + }); + return { link, aspectRatio, type, warning, sandbox: { allowSameOrigin } }; } const figmaLink = link.match(RE_FIGMA); @@ -133,16 +163,26 @@ export const getEmbedLink = (link: string | null | undefined): EmbeddedLink => { link, )}`; aspectRatio = { w: 550, h: 550 }; - embeddedLinkCache.set(originalLink, { link, aspectRatio, type }); - return { link, aspectRatio, type }; + embeddedLinkCache.set(originalLink, { + link, + aspectRatio, + type, + sandbox: { allowSameOrigin }, + }); + return { link, aspectRatio, type, sandbox: { allowSameOrigin } }; } const valLink = link.match(RE_VALTOWN); if (valLink) { link = valLink[1] === "embed" ? valLink[0] : valLink[0].replace("/v", "/embed"); - embeddedLinkCache.set(originalLink, { link, aspectRatio, type }); - return { link, aspectRatio, type }; + embeddedLinkCache.set(originalLink, { + link, + aspectRatio, + type, + sandbox: { allowSameOrigin }, + }); + return { link, aspectRatio, type, sandbox: { allowSameOrigin } }; } if (RE_TWITTER.test(link)) { @@ -162,7 +202,7 @@ export const getEmbedLink = (link: string | null | undefined): EmbeddedLink => { `
`, ), aspectRatio: { w: 480, h: 480 }, - sandbox: { allowSameOrigin: true }, + sandbox: { allowSameOrigin }, }; embeddedLinkCache.set(originalLink, ret); return ret; @@ -185,13 +225,19 @@ export const getEmbedLink = (link: string | null | undefined): EmbeddedLink => { `), aspectRatio: { w: 550, h: 720 }, + sandbox: { allowSameOrigin }, }; embeddedLinkCache.set(link, ret); return ret; } - embeddedLinkCache.set(link, { link, aspectRatio, type }); - return { link, aspectRatio, type }; + embeddedLinkCache.set(link, { + link, + aspectRatio, + type, + sandbox: { allowSameOrigin }, + }); + return { link, aspectRatio, type, sandbox: { allowSameOrigin } }; }; export const isEmbeddableOrLabel = ( @@ -266,34 +312,39 @@ export const actionSetEmbeddableAsActiveTool = register({ }, }); -const validateHostname = ( +const matchHostname = ( url: string, /** using a Set assumes it already contains normalized bare domains */ allowedHostnames: Set | string, -): boolean => { +): string | null => { try { const { hostname } = new URL(url); const bareDomain = hostname.replace(/^www\./, ""); - const bareDomainWithFirstSubdomainWildcarded = bareDomain.replace( - /^([^.]+)/, - "*", - ); if (allowedHostnames instanceof Set) { - return ( - ALLOWED_DOMAINS.has(bareDomain) || - ALLOWED_DOMAINS.has(bareDomainWithFirstSubdomainWildcarded) + if (ALLOWED_DOMAINS.has(bareDomain)) { + return bareDomain; + } + + const bareDomainWithFirstSubdomainWildcarded = bareDomain.replace( + /^([^.]+)/, + "*", ); + if (ALLOWED_DOMAINS.has(bareDomainWithFirstSubdomainWildcarded)) { + return bareDomainWithFirstSubdomainWildcarded; + } + return null; } - if (bareDomain === allowedHostnames.replace(/^www\./, "")) { - return true; + const bareAllowedHostname = allowedHostnames.replace(/^www\./, ""); + if (bareDomain === bareAllowedHostname) { + return bareAllowedHostname; } } catch (error) { // ignore } - return false; + return null; }; export const extractSrc = (htmlString: string): string => { @@ -342,7 +393,7 @@ export const embeddableURLValidator = ( if (url.match(domain)) { return true; } - } else if (validateHostname(url, domain)) { + } else if (matchHostname(url, domain)) { return true; } } @@ -350,5 +401,5 @@ export const embeddableURLValidator = ( } } - return validateHostname(url, ALLOWED_DOMAINS); + return !!matchHostname(url, ALLOWED_DOMAINS); };