Pārlūkot izejas kodu

fix: parse embeddable srcdoc urls strictly & escape attribute url html

dwelle 1 gadu atpakaļ
vecāks
revīzija
58338e54c9
2 mainītis faili ar 26 papildinājumiem un 10 dzēšanām
  1. 5 1
      src/data/url.ts
  2. 21 9
      src/element/embeddable.ts

+ 5 - 1
src/data/url.ts

@@ -1,11 +1,15 @@
 import { sanitizeUrl } from "@braintree/sanitize-url";
 import { sanitizeUrl } from "@braintree/sanitize-url";
 
 
+export const sanitizeHTMLAttribute = (html: string) => {
+  return html.replace(/"/g, """);
+};
+
 export const normalizeLink = (link: string) => {
 export const normalizeLink = (link: string) => {
   link = link.trim();
   link = link.trim();
   if (!link) {
   if (!link) {
     return link;
     return link;
   }
   }
-  return sanitizeUrl(link);
+  return sanitizeUrl(sanitizeHTMLAttribute(link));
 };
 };
 
 
 export const isLocalLink = (link: string | null) => {
 export const isLocalLink = (link: string | null) => {

+ 21 - 9
src/element/embeddable.ts

@@ -13,6 +13,7 @@ import {
   NonDeletedExcalidrawElement,
   NonDeletedExcalidrawElement,
   Theme,
   Theme,
 } from "./types";
 } from "./types";
+import { sanitizeHTMLAttribute } from "../data/url";
 
 
 type EmbeddedLink =
 type EmbeddedLink =
   | ({
   | ({
@@ -34,12 +35,13 @@ const RE_VIMEO =
   /^(?:http(?:s)?:\/\/)?(?:(?:w){3}\.)?(?:player\.)?vimeo\.com\/(?:video\/)?([^?\s]+)(?:\?.*)?$/;
   /^(?:http(?:s)?:\/\/)?(?:(?:w){3}\.)?(?:player\.)?vimeo\.com\/(?:video\/)?([^?\s]+)(?:\?.*)?$/;
 const RE_FIGMA = /^https:\/\/(?:www\.)?figma\.com/;
 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 =
 const RE_GH_GIST_EMBED =
-  /https?:\/\/gist\.github\.com\/([\w_-]+)\/([\w_-]+)\.js["']/i;
+  /^<script[\s\S]*?\ssrc=["'](https:\/\/gist\.github\.com\/.*?)\.js["']/i;
 
 
 // not anchored to start to allow <blockquote> twitter embeds
 // not anchored to start to allow <blockquote> 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 =
 const RE_TWITTER_EMBED =
   /^<blockquote[\s\S]*?\shref=["'](https?:\/\/(?:twitter|x)\.com\/[^"']*)/i;
   /^<blockquote[\s\S]*?\shref=["'](https?:\/\/(?:twitter|x)\.com\/[^"']*)/i;
 
 
@@ -144,14 +146,20 @@ export const getEmbedLink = (link: string | null | undefined): EmbeddedLink => {
   }
   }
 
 
   if (RE_TWITTER.test(link)) {
   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 = {
     const ret: EmbeddedLink = {
       type: "document",
       type: "document",
       srcdoc: (theme: string) =>
       srcdoc: (theme: string) =>
         createSrcDoc(
         createSrcDoc(
-          `<blockquote class="twitter-tweet" data-dnt="true" data-theme="${theme}"><a href="${link}"></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>`,
         ),
         ),
       aspectRatio: { w: 480, h: 480 },
       aspectRatio: { w: 480, h: 480 },
       sandbox: { allowSameOrigin: true },
       sandbox: { allowSameOrigin: true },
@@ -161,11 +169,15 @@ export const getEmbedLink = (link: string | null | undefined): EmbeddedLink => {
   }
   }
 
 
   if (RE_GH_GIST.test(link)) {
   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 = {
     const ret: EmbeddedLink = {
       type: "document",
       type: "document",
       srcdoc: () =>
       srcdoc: () =>
         createSrcDoc(`
         createSrcDoc(`
-          <script src="${link}.js"></script>
+          <script src="${safeURL}.js"></script>
           <style type="text/css">
           <style type="text/css">
             * { margin: 0px; }
             * { margin: 0px; }
             table, .gist { height: 100%; }
             table, .gist { height: 100%; }
@@ -291,8 +303,8 @@ export const extractSrc = (htmlString: string): string => {
   }
   }
 
 
   const gistMatch = htmlString.match(RE_GH_GIST_EMBED);
   const gistMatch = htmlString.match(RE_GH_GIST_EMBED);
-  if (gistMatch && gistMatch.length === 3) {
-    return `https://gist.github.com/${gistMatch[1]}/${gistMatch[2]}`;
+  if (gistMatch && gistMatch.length === 2) {
+    return gistMatch[1];
   }
   }
 
 
   if (RE_GIPHY.test(htmlString)) {
   if (RE_GIPHY.test(htmlString)) {