fix: allow same origin for all necessary domains (#7889)

This commit is contained in:
David Luzar 2024-04-13 18:51:30 +02:00 committed by GitHub
parent f59b4f6af4
commit da2e507298
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -12,8 +12,11 @@ import {
IframeData, IframeData,
} from "./types"; } from "./types";
import { sanitizeHTMLAttribute } from "../data/url"; import { sanitizeHTMLAttribute } from "../data/url";
import { MarkRequired } from "../utility-types";
const embeddedLinkCache = new Map<string, IframeData>(); type IframeDataWithSandbox = MarkRequired<IframeData, "sandbox">;
const embeddedLinkCache = new Map<string, IframeDataWithSandbox>();
const RE_YOUTUBE = const RE_YOUTUBE =
/^(?:http(?:s)?:\/\/)?(?:www\.)?youtu(?:be\.com|\.be)\/(embed\/|watch\?v=|shorts\/|playlist\?list=|embed\/videoseries\?list=)?([a-zA-Z0-9_-]+)(?:\?t=|&t=|\?start=|&start=)?([a-zA-Z0-9_-]+)?[^\s]*$/; /^(?:http(?:s)?:\/\/)?(?:www\.)?youtu(?:be\.com|\.be)\/(embed\/|watch\?v=|shorts\/|playlist\?list=|embed\/videoseries\?list=)?([a-zA-Z0-9_-]+)(?:\?t=|&t=|\?start=|&start=)?([a-zA-Z0-9_-]+)?[^\s]*$/;
@ -55,7 +58,18 @@ const ALLOWED_DOMAINS = new Set([
"stackblitz.com", "stackblitz.com",
"val.town", "val.town",
"giphy.com", "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",
]); ]);
export const createSrcDoc = (body: string) => { export const createSrcDoc = (body: string) => {
@ -64,7 +78,7 @@ export const createSrcDoc = (body: string) => {
export const getEmbedLink = ( export const getEmbedLink = (
link: string | null | undefined, link: string | null | undefined,
): IframeData | null => { ): IframeDataWithSandbox | null => {
if (!link) { if (!link) {
return null; return null;
} }
@ -75,6 +89,10 @@ export const getEmbedLink = (
const originalLink = link; const originalLink = link;
const allowSameOrigin = ALLOW_SAME_ORIGIN.has(
matchHostname(link, ALLOW_SAME_ORIGIN) || "",
);
let type: "video" | "generic" = "generic"; let type: "video" | "generic" = "generic";
let aspectRatio = { w: 560, h: 840 }; let aspectRatio = { w: 560, h: 840 };
const ytLink = link.match(RE_YOUTUBE); const ytLink = link.match(RE_YOUTUBE);
@ -101,8 +119,14 @@ export const getEmbedLink = (
link, link,
intrinsicSize: aspectRatio, intrinsicSize: aspectRatio,
type, type,
sandbox: { allowSameOrigin },
}); });
return { link, intrinsicSize: aspectRatio, type }; return {
link,
intrinsicSize: aspectRatio,
type,
sandbox: { allowSameOrigin },
};
} }
const vimeoLink = link.match(RE_VIMEO); const vimeoLink = link.match(RE_VIMEO);
@ -120,8 +144,15 @@ export const getEmbedLink = (
link, link,
intrinsicSize: aspectRatio, intrinsicSize: aspectRatio,
type, type,
sandbox: { allowSameOrigin },
}); });
return { link, intrinsicSize: aspectRatio, type, error }; return {
link,
intrinsicSize: aspectRatio,
type,
error,
sandbox: { allowSameOrigin },
};
} }
const figmaLink = link.match(RE_FIGMA); const figmaLink = link.match(RE_FIGMA);
@ -135,8 +166,14 @@ export const getEmbedLink = (
link, link,
intrinsicSize: aspectRatio, intrinsicSize: aspectRatio,
type, type,
sandbox: { allowSameOrigin },
}); });
return { link, intrinsicSize: aspectRatio, type }; return {
link,
intrinsicSize: aspectRatio,
type,
sandbox: { allowSameOrigin },
};
} }
const valLink = link.match(RE_VALTOWN); const valLink = link.match(RE_VALTOWN);
@ -147,8 +184,14 @@ export const getEmbedLink = (
link, link,
intrinsicSize: aspectRatio, intrinsicSize: aspectRatio,
type, type,
sandbox: { allowSameOrigin },
}); });
return { link, intrinsicSize: aspectRatio, type }; return {
link,
intrinsicSize: aspectRatio,
type,
sandbox: { allowSameOrigin },
};
} }
if (RE_TWITTER.test(link)) { if (RE_TWITTER.test(link)) {
@ -161,14 +204,14 @@ export const getEmbedLink = (
`https://twitter.com/x/status/${postId}`, `https://twitter.com/x/status/${postId}`,
); );
const ret: IframeData = { const ret: IframeDataWithSandbox = {
type: "document", type: "document",
srcdoc: (theme: string) => srcdoc: (theme: string) =>
createSrcDoc( createSrcDoc(
`<blockquote class="twitter-tweet" data-dnt="true" data-theme="${theme}"><a href="${safeURL}"></a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>`, `<blockquote class="twitter-tweet" data-dnt="true" data-theme="${theme}"><a href="${safeURL}"></a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>`,
), ),
intrinsicSize: { w: 480, h: 480 }, intrinsicSize: { w: 480, h: 480 },
sandbox: { allowSameOrigin: true }, sandbox: { allowSameOrigin },
}; };
embeddedLinkCache.set(originalLink, ret); embeddedLinkCache.set(originalLink, ret);
return ret; return ret;
@ -179,7 +222,7 @@ export const getEmbedLink = (
const safeURL = sanitizeHTMLAttribute( const safeURL = sanitizeHTMLAttribute(
`https://gist.github.com/${user}/${gistId}`, `https://gist.github.com/${user}/${gistId}`,
); );
const ret: IframeData = { const ret: IframeDataWithSandbox = {
type: "document", type: "document",
srcdoc: () => srcdoc: () =>
createSrcDoc(` createSrcDoc(`
@ -191,13 +234,24 @@ export const getEmbedLink = (
</style> </style>
`), `),
intrinsicSize: { w: 550, h: 720 }, intrinsicSize: { w: 550, h: 720 },
sandbox: { allowSameOrigin },
}; };
embeddedLinkCache.set(link, ret); embeddedLinkCache.set(link, ret);
return ret; return ret;
} }
embeddedLinkCache.set(link, { link, intrinsicSize: aspectRatio, type }); embeddedLinkCache.set(link, {
return { link, intrinsicSize: aspectRatio, type }; link,
intrinsicSize: aspectRatio,
type,
sandbox: { allowSameOrigin },
});
return {
link,
intrinsicSize: aspectRatio,
type,
sandbox: { allowSameOrigin },
};
}; };
export const createPlaceholderEmbeddableLabel = ( export const createPlaceholderEmbeddableLabel = (
@ -265,34 +319,39 @@ export const actionSetEmbeddableAsActiveTool = register({
}, },
}); });
const validateHostname = ( const matchHostname = (
url: string, url: string,
/** using a Set assumes it already contains normalized bare domains */ /** using a Set assumes it already contains normalized bare domains */
allowedHostnames: Set<string> | string, allowedHostnames: Set<string> | string,
): boolean => { ): string | null => {
try { try {
const { hostname } = new URL(url); const { hostname } = new URL(url);
const bareDomain = hostname.replace(/^www\./, ""); const bareDomain = hostname.replace(/^www\./, "");
if (allowedHostnames instanceof Set) {
if (ALLOWED_DOMAINS.has(bareDomain)) {
return bareDomain;
}
const bareDomainWithFirstSubdomainWildcarded = bareDomain.replace( const bareDomainWithFirstSubdomainWildcarded = bareDomain.replace(
/^([^.]+)/, /^([^.]+)/,
"*", "*",
); );
if (ALLOWED_DOMAINS.has(bareDomainWithFirstSubdomainWildcarded)) {
if (allowedHostnames instanceof Set) { return bareDomainWithFirstSubdomainWildcarded;
return ( }
ALLOWED_DOMAINS.has(bareDomain) || return null;
ALLOWED_DOMAINS.has(bareDomainWithFirstSubdomainWildcarded)
);
} }
if (bareDomain === allowedHostnames.replace(/^www\./, "")) { const bareAllowedHostname = allowedHostnames.replace(/^www\./, "");
return true; if (bareDomain === bareAllowedHostname) {
return bareAllowedHostname;
} }
} catch (error) { } catch (error) {
// ignore // ignore
} }
return false; return null;
}; };
export const maybeParseEmbedSrc = (str: string): string => { export const maybeParseEmbedSrc = (str: string): string => {
@ -342,7 +401,7 @@ export const embeddableURLValidator = (
if (url.match(domain)) { if (url.match(domain)) {
return true; return true;
} }
} else if (validateHostname(url, domain)) { } else if (matchHostname(url, domain)) {
return true; return true;
} }
} }
@ -350,5 +409,5 @@ export const embeddableURLValidator = (
} }
} }
return validateHostname(url, ALLOWED_DOMAINS); return !!matchHostname(url, ALLOWED_DOMAINS);
}; };