From 58338e54c928ba3ba45ebf98202ed88ec6cfb7b1 Mon Sep 17 00:00:00 2001 From: dwelle <5153846+dwelle@users.noreply.github.com> Date: Fri, 12 Apr 2024 19:27:41 +0200 Subject: [PATCH] fix: parse embeddable srcdoc urls strictly & escape attribute url html --- src/data/url.ts | 6 +++++- src/element/embeddable.ts | 30 +++++++++++++++++++++--------- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/data/url.ts b/src/data/url.ts index 2655c141d..dae576068 100644 --- a/src/data/url.ts +++ b/src/data/url.ts @@ -1,11 +1,15 @@ import { sanitizeUrl } from "@braintree/sanitize-url"; +export const sanitizeHTMLAttribute = (html: string) => { + return html.replace(/"/g, """); +}; + export const normalizeLink = (link: string) => { link = link.trim(); if (!link) { return link; } - return sanitizeUrl(link); + return sanitizeUrl(sanitizeHTMLAttribute(link)); }; export const isLocalLink = (link: string | null) => { diff --git a/src/element/embeddable.ts b/src/element/embeddable.ts index 81de4e90c..0010246fb 100644 --- a/src/element/embeddable.ts +++ b/src/element/embeddable.ts @@ -13,6 +13,7 @@ import { NonDeletedExcalidrawElement, Theme, } from "./types"; +import { sanitizeHTMLAttribute } from "../data/url"; type EmbeddedLink = | ({ @@ -34,12 +35,13 @@ const RE_VIMEO = /^(?:http(?:s)?:\/\/)?(?:(?:w){3}\.)?(?:player\.)?vimeo\.com\/(?:video\/)?([^?\s]+)(?:\?.*)?$/; const RE_FIGMA = /^https:\/\/(?:www\.)?figma\.com/; -const RE_GH_GIST = /^https:\/\/gist\.github\.com/; +const RE_GH_GIST = /^https:\/\/gist\.github\.com\/([\w_-]+)\/([\w_-]+)/; const RE_GH_GIST_EMBED = - /https?:\/\/gist\.github\.com\/([\w_-]+)\/([\w_-]+)\.js["']/i; + /^ twitter embeds -const RE_TWITTER = /(?:https?:\/\/)?(?:(?:w){3}\.)?(?:twitter|x)\.com/; +const RE_TWITTER = + /(?:https?:\/\/)?(?:(?:w){3}\.)?(?:twitter|x)\.com\/[^/]+\/status\/(\d+)/; const RE_TWITTER_EMBED = /^ { } if (RE_TWITTER.test(link)) { - // the embed srcdoc still supports twitter.com domain only - link = link.replace(/\bx.com\b/, "twitter.com"); + const postId = link.match(RE_TWITTER)![1]; + // the embed srcdoc still supports twitter.com domain only. + // Note that we don't attempt to parse the username as it can consist of + // non-latin1 characters, and the username in the url can be set to anything + // without affecting the embed. + const safeURL = sanitizeHTMLAttribute( + `https://twitter.com/x/status/${postId}`, + ); const ret: EmbeddedLink = { type: "document", srcdoc: (theme: string) => createSrcDoc( - ` `, + ` `, ), aspectRatio: { w: 480, h: 480 }, sandbox: { allowSameOrigin: true }, @@ -161,11 +169,15 @@ export const getEmbedLink = (link: string | null | undefined): EmbeddedLink => { } if (RE_GH_GIST.test(link)) { + const [, user, gistId] = link.match(RE_GH_GIST)!; + const safeURL = sanitizeHTMLAttribute( + `https://gist.github.com/${user}/${gistId}`, + ); const ret: EmbeddedLink = { type: "document", srcdoc: () => createSrcDoc(` - +